@fedify/vocab 2.4.0-pr.936.41 → 2.4.1
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 +32014 -9323
- package/dist/mod.d.cts +4770 -91
- package/dist/mod.d.ts +4770 -91
- package/dist/mod.js +31907 -9223
- package/dist-tests/actor-D_QaCMro.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-YWo4aQQe.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-BevjwZhB.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-DaniQwU7.mjs} +31669 -9267
- package/dist-tests/vocab.test.mjs +901 -11
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +291 -15
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/gateway.test.ts +2637 -0
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +1 -1
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/vocab.test.ts +1219 -5
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
globalThis.addEventListener = () => {};
|
|
3
|
+
import { M as OrderedCollection, X as vocab_exports, i as Announce, k as Note, u as Create, z as Question } from "./vocab-DaniQwU7.mjs";
|
|
4
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
5
|
+
import { deepStrictEqual, ok } from "node:assert/strict";
|
|
6
|
+
import { isMarkerSafeContext, retainSignedRepresentation } from "@fedify/vocab-runtime/internal/signed-representation";
|
|
7
|
+
//#region src/signed-representation.test.ts
|
|
8
|
+
const securedNote = {
|
|
9
|
+
"@context": [
|
|
10
|
+
"https://www.w3.org/ns/activitystreams",
|
|
11
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
12
|
+
{ ex: "https://example.com/ns#" }
|
|
13
|
+
],
|
|
14
|
+
id: "https://example.com/notes/1",
|
|
15
|
+
type: "Note",
|
|
16
|
+
content: "Hello",
|
|
17
|
+
proof: {
|
|
18
|
+
"@context": [
|
|
19
|
+
"https://www.w3.org/ns/activitystreams",
|
|
20
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
21
|
+
{ ex: "https://example.com/ns#" }
|
|
22
|
+
],
|
|
23
|
+
type: "DataIntegrityProof",
|
|
24
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
25
|
+
created: "2023-02-24T23:36:38Z",
|
|
26
|
+
verificationMethod: "https://example.com/person#ed25519-key",
|
|
27
|
+
proofPurpose: "assertionMethod",
|
|
28
|
+
proofValue: "z3FXQ"
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const options = { contextLoader: mockDocumentLoader };
|
|
32
|
+
function retainedNote() {
|
|
33
|
+
const note = new Note({
|
|
34
|
+
id: new URL("https://example.com/notes/1"),
|
|
35
|
+
content: "Hello"
|
|
36
|
+
});
|
|
37
|
+
retainSignedRepresentation(note, structuredClone(securedNote));
|
|
38
|
+
return note;
|
|
39
|
+
}
|
|
40
|
+
test("nested serialization embeds a retained signed representation", async () => {
|
|
41
|
+
deepStrictEqual((await new Create({
|
|
42
|
+
id: new URL("https://example.com/activities/1"),
|
|
43
|
+
actor: new URL("https://example.com/person"),
|
|
44
|
+
object: retainedNote()
|
|
45
|
+
}).toJsonLd({
|
|
46
|
+
format: "compact",
|
|
47
|
+
...options
|
|
48
|
+
})).object, securedNote);
|
|
49
|
+
});
|
|
50
|
+
test("nested serialization embeds a retained representation through a compactable parent", async () => {
|
|
51
|
+
const wrapper = new Note({
|
|
52
|
+
id: new URL("https://example.com/notes/wrapper"),
|
|
53
|
+
attachments: [retainedNote()]
|
|
54
|
+
});
|
|
55
|
+
deepStrictEqual((await wrapper.toJsonLd(options)).attachment, securedNote);
|
|
56
|
+
const embeddedWrapper = (await new Question({
|
|
57
|
+
id: new URL("https://example.com/questions/1"),
|
|
58
|
+
exclusiveOptions: [wrapper]
|
|
59
|
+
}).toJsonLd(options)).oneOf;
|
|
60
|
+
deepStrictEqual(embeddedWrapper.attachment, securedNote);
|
|
61
|
+
});
|
|
62
|
+
test("nested serialization embeds one retained representation many times", async () => {
|
|
63
|
+
const note = retainedNote();
|
|
64
|
+
const compact = await new Create({
|
|
65
|
+
id: new URL("https://example.com/activities/1"),
|
|
66
|
+
actor: new URL("https://example.com/person"),
|
|
67
|
+
objects: [note, note],
|
|
68
|
+
tags: [note]
|
|
69
|
+
}).toJsonLd({
|
|
70
|
+
format: "compact",
|
|
71
|
+
...options
|
|
72
|
+
});
|
|
73
|
+
const objects = compact.object;
|
|
74
|
+
deepStrictEqual(objects.length, 2);
|
|
75
|
+
deepStrictEqual(objects[0], securedNote);
|
|
76
|
+
deepStrictEqual(objects[1], securedNote);
|
|
77
|
+
deepStrictEqual(compact.tag, securedNote);
|
|
78
|
+
ok(objects[0] !== objects[1]);
|
|
79
|
+
ok(objects[0] !== compact.tag);
|
|
80
|
+
});
|
|
81
|
+
test("nested serialization embeds a retained representation at any depth", async () => {
|
|
82
|
+
const create = (await new Announce({
|
|
83
|
+
id: new URL("https://example.com/activities/2"),
|
|
84
|
+
actor: new URL("https://example.com/person"),
|
|
85
|
+
object: new Create({
|
|
86
|
+
id: new URL("https://example.com/activities/1"),
|
|
87
|
+
actor: new URL("https://example.com/person"),
|
|
88
|
+
object: retainedNote()
|
|
89
|
+
})
|
|
90
|
+
}).toJsonLd({
|
|
91
|
+
format: "compact",
|
|
92
|
+
...options
|
|
93
|
+
})).object;
|
|
94
|
+
deepStrictEqual(create.object, securedNote);
|
|
95
|
+
});
|
|
96
|
+
test("expanded serialization ignores a retained signed representation", async () => {
|
|
97
|
+
const objects = (await new Create({
|
|
98
|
+
id: new URL("https://example.com/activities/1"),
|
|
99
|
+
actor: new URL("https://example.com/person"),
|
|
100
|
+
object: retainedNote()
|
|
101
|
+
}).toJsonLd({
|
|
102
|
+
format: "expand",
|
|
103
|
+
...options
|
|
104
|
+
}))[0]["https://www.w3.org/ns/activitystreams#object"];
|
|
105
|
+
deepStrictEqual(objects[0]["@id"], "https://example.com/notes/1");
|
|
106
|
+
deepStrictEqual(objects[0]["@type"], ["https://www.w3.org/ns/activitystreams#Note"]);
|
|
107
|
+
});
|
|
108
|
+
test("a context that could hide a placeholder turns retention off", async () => {
|
|
109
|
+
const embedded = (await new Create({
|
|
110
|
+
id: new URL("https://example.com/activities/1"),
|
|
111
|
+
actor: new URL("https://example.com/person"),
|
|
112
|
+
object: retainedNote()
|
|
113
|
+
}).toJsonLd({
|
|
114
|
+
format: "compact",
|
|
115
|
+
context: [
|
|
116
|
+
"https://www.w3.org/ns/activitystreams",
|
|
117
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
118
|
+
{ identifier: "@id" }
|
|
119
|
+
],
|
|
120
|
+
...options
|
|
121
|
+
})).object;
|
|
122
|
+
deepStrictEqual(embedded["@context"], void 0);
|
|
123
|
+
deepStrictEqual(embedded.proof, void 0);
|
|
124
|
+
});
|
|
125
|
+
test("a clone does not inherit a retained signed representation", async () => {
|
|
126
|
+
const note = retainedNote();
|
|
127
|
+
const embedded = (await new Create({
|
|
128
|
+
id: new URL("https://example.com/activities/1"),
|
|
129
|
+
actor: new URL("https://example.com/person"),
|
|
130
|
+
object: note.clone({ content: "Changed" })
|
|
131
|
+
}).toJsonLd({
|
|
132
|
+
format: "compact",
|
|
133
|
+
...options
|
|
134
|
+
})).object;
|
|
135
|
+
deepStrictEqual(embedded["@context"], void 0);
|
|
136
|
+
deepStrictEqual(embedded.content, "Changed");
|
|
137
|
+
});
|
|
138
|
+
test("every generated default context keeps placeholders recoverable", async () => {
|
|
139
|
+
const classes = Object.values(vocab_exports).filter((value) => typeof value === "function" && value.typeId instanceof URL);
|
|
140
|
+
ok(classes.length > 0);
|
|
141
|
+
for (const cls of classes) ok(isMarkerSafeContext((await new cls({ id: new URL("https://example.com/objects/1") }).toJsonLd())["@context"]), `${cls.typeId.href} has a default context that could hide a placeholder`);
|
|
142
|
+
});
|
|
143
|
+
test("a subtype that redefines an inherited property still embeds a retained representation", async () => {
|
|
144
|
+
const compact = await new OrderedCollection({
|
|
145
|
+
id: new URL("https://example.com/collections/1"),
|
|
146
|
+
items: [retainedNote()]
|
|
147
|
+
}).toJsonLd(options);
|
|
148
|
+
deepStrictEqual(compact.items, void 0);
|
|
149
|
+
deepStrictEqual(compact.orderedItems, [securedNote]);
|
|
150
|
+
});
|
|
151
|
+
test("a context that shadows the placeholder scheme turns retention off", async () => {
|
|
152
|
+
const embedded = (await new Create({
|
|
153
|
+
id: new URL("https://example.com/activities/1"),
|
|
154
|
+
actor: new URL("https://example.com/person"),
|
|
155
|
+
object: retainedNote()
|
|
156
|
+
}).toJsonLd({
|
|
157
|
+
format: "compact",
|
|
158
|
+
context: [
|
|
159
|
+
"https://www.w3.org/ns/activitystreams",
|
|
160
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
161
|
+
{ urn: "https://example.com/ns#" }
|
|
162
|
+
],
|
|
163
|
+
...options
|
|
164
|
+
})).object;
|
|
165
|
+
deepStrictEqual(embedded["@context"], void 0);
|
|
166
|
+
deepStrictEqual(embedded.proof, void 0);
|
|
167
|
+
});
|
|
168
|
+
test("a context that aliases `@id` indirectly turns retention off", async () => {
|
|
169
|
+
const embedded = (await new Create({
|
|
170
|
+
id: new URL("https://example.com/activities/1"),
|
|
171
|
+
actor: new URL("https://example.com/person"),
|
|
172
|
+
object: retainedNote()
|
|
173
|
+
}).toJsonLd({
|
|
174
|
+
format: "compact",
|
|
175
|
+
context: [
|
|
176
|
+
"https://www.w3.org/ns/activitystreams",
|
|
177
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
178
|
+
{
|
|
179
|
+
i: "id",
|
|
180
|
+
object: "as:object"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
...options
|
|
184
|
+
})).object;
|
|
185
|
+
deepStrictEqual(embedded["@context"], void 0);
|
|
186
|
+
deepStrictEqual(embedded.proof, void 0);
|
|
187
|
+
});
|
|
188
|
+
//#endregion
|
|
189
|
+
export {};
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
globalThis.addEventListener = () => {};
|
|
3
|
+
import { A as Object$1, E as Link, F as Person, K as Translation, L as PropertyValue, P as Organization, U as Service, a as Application, b as Group, k as Note, o as Article, q as Update, s as Collection, u as Create } from "./vocab-DaniQwU7.mjs";
|
|
4
|
+
import "./actor-D_QaCMro.mjs";
|
|
5
|
+
import "./lookup-BevjwZhB.mjs";
|
|
6
|
+
import { test } from "@fedify/fixture";
|
|
7
|
+
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
8
|
+
import { LanguageString as LanguageString$1, getDocumentLoader, preloadedContexts } from "@fedify/vocab-runtime";
|
|
9
|
+
new URL("https://www.w3.org/ns/activitystreams#Public");
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/translation.test.ts
|
|
12
|
+
const { Temporal: TemporalPolyfill } = await import("temporal-polyfill");
|
|
13
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
14
|
+
const FEP = "https://w3id.org/fep/22cd";
|
|
15
|
+
const SCHEMA = "https://schema.org/";
|
|
16
|
+
const articleId = new URL("https://example.com/articles/1");
|
|
17
|
+
const alice = new URL("https://example.com/users/alice");
|
|
18
|
+
const context = [AS, FEP];
|
|
19
|
+
const sourceUpdated = TemporalPolyfill.Instant.from("2026-09-01T00:00:00Z");
|
|
20
|
+
const entry = {
|
|
21
|
+
type: "Translation",
|
|
22
|
+
inLanguage: "ko",
|
|
23
|
+
url: "https://example.com/articles/1/ko",
|
|
24
|
+
translator: [alice.href],
|
|
25
|
+
translationOfWork: articleId.href,
|
|
26
|
+
sourceUpdated: sourceUpdated.toString()
|
|
27
|
+
};
|
|
28
|
+
const example = {
|
|
29
|
+
"@context": context,
|
|
30
|
+
id: articleId.href,
|
|
31
|
+
type: "Article",
|
|
32
|
+
attributedTo: alice.href,
|
|
33
|
+
updated: sourceUpdated.toString(),
|
|
34
|
+
contentMap: {
|
|
35
|
+
en: "<p>Original text...</p>",
|
|
36
|
+
ko: "<p>번역 텍스트...</p>"
|
|
37
|
+
},
|
|
38
|
+
translations: [entry]
|
|
39
|
+
};
|
|
40
|
+
function translation() {
|
|
41
|
+
return new Translation({
|
|
42
|
+
language: new Intl.Locale("ko"),
|
|
43
|
+
translator: alice,
|
|
44
|
+
original: articleId,
|
|
45
|
+
sourceUpdated,
|
|
46
|
+
url: new URL(entry.url)
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function article() {
|
|
50
|
+
return new Article({
|
|
51
|
+
id: articleId,
|
|
52
|
+
attribution: alice,
|
|
53
|
+
updated: sourceUpdated,
|
|
54
|
+
contents: [new LanguageString$1(example.contentMap.en, "en"), new LanguageString$1(example.contentMap.ko, "ko")],
|
|
55
|
+
translations: [translation()]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function record(value) {
|
|
59
|
+
ok(value != null && typeof value === "object" && !Array.isArray(value));
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
const noFetch = (url) => {
|
|
63
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
64
|
+
};
|
|
65
|
+
function actorLoader(type, id) {
|
|
66
|
+
return (url) => Promise.resolve({
|
|
67
|
+
contextUrl: null,
|
|
68
|
+
documentUrl: url,
|
|
69
|
+
document: {
|
|
70
|
+
"@context": AS,
|
|
71
|
+
type,
|
|
72
|
+
id: id ?? url
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
test("FEP-22cd canonical context is preloaded", async () => {
|
|
77
|
+
ok(FEP in preloadedContexts);
|
|
78
|
+
deepStrictEqual(record((await getDocumentLoader()(FEP)).document)["@context"], {
|
|
79
|
+
"fep-22cd": `${FEP}#`,
|
|
80
|
+
schema: SCHEMA,
|
|
81
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
82
|
+
translations: {
|
|
83
|
+
"@id": "fep-22cd:translations",
|
|
84
|
+
"@container": "@set"
|
|
85
|
+
},
|
|
86
|
+
Translation: "fep-22cd:Translation",
|
|
87
|
+
sourceUpdated: {
|
|
88
|
+
"@id": "fep-22cd:sourceUpdated",
|
|
89
|
+
"@type": "xsd:dateTime"
|
|
90
|
+
},
|
|
91
|
+
translator: {
|
|
92
|
+
"@id": "schema:translator",
|
|
93
|
+
"@type": "@id",
|
|
94
|
+
"@container": "@set"
|
|
95
|
+
},
|
|
96
|
+
inLanguage: "schema:inLanguage",
|
|
97
|
+
translationOfWork: {
|
|
98
|
+
"@id": "schema:translationOfWork",
|
|
99
|
+
"@type": "@id"
|
|
100
|
+
},
|
|
101
|
+
isBasedOn: {
|
|
102
|
+
"@id": "schema:isBasedOn",
|
|
103
|
+
"@type": "@id"
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
test("FEP-22cd personal translation compact and expanded round trips", async () => {
|
|
108
|
+
for (const format of [
|
|
109
|
+
void 0,
|
|
110
|
+
"compact",
|
|
111
|
+
"expand"
|
|
112
|
+
]) {
|
|
113
|
+
const encoded = await article().toJsonLd({ format });
|
|
114
|
+
if (format !== "expand") {
|
|
115
|
+
deepStrictEqual(record(encoded).translations, [entry]);
|
|
116
|
+
ok(record(encoded)["@context"].includes(FEP));
|
|
117
|
+
}
|
|
118
|
+
const restored = await Article.fromJsonLd(encoded, { documentLoader: noFetch });
|
|
119
|
+
deepStrictEqual(restored.attributionId, alice);
|
|
120
|
+
deepStrictEqual(restored.contents, article().contents);
|
|
121
|
+
deepStrictEqual(restored.translations.length, 1);
|
|
122
|
+
const t = restored.translations[0];
|
|
123
|
+
deepStrictEqual(t.language?.baseName, "ko");
|
|
124
|
+
deepStrictEqual(t.original, articleId);
|
|
125
|
+
deepStrictEqual(t.translatorIds, [alice]);
|
|
126
|
+
deepStrictEqual(t.sourceUpdated?.toString(), sourceUpdated.toString());
|
|
127
|
+
deepStrictEqual(t.url, new URL(entry.url));
|
|
128
|
+
deepStrictEqual(record(await restored.toJsonLd({ format: "compact" })).translations, [entry]);
|
|
129
|
+
}
|
|
130
|
+
deepStrictEqual(record(await (await Article.fromJsonLd(example, { documentLoader: noFetch })).toJsonLd({
|
|
131
|
+
format: "compact",
|
|
132
|
+
context
|
|
133
|
+
})).translations, [entry]);
|
|
134
|
+
deepStrictEqual(await article().toJsonLd({
|
|
135
|
+
format: "compact",
|
|
136
|
+
context
|
|
137
|
+
}), example);
|
|
138
|
+
});
|
|
139
|
+
test("FEP-22cd full IRIs, optional revision, script language and AS links", async () => {
|
|
140
|
+
const expanded = {
|
|
141
|
+
"@type": `${FEP}#Translation`,
|
|
142
|
+
[`${SCHEMA}inLanguage`]: "zh-Hant",
|
|
143
|
+
[`${SCHEMA}translator`]: [{ "@id": alice.href }],
|
|
144
|
+
[`${SCHEMA}translationOfWork`]: { "@id": articleId.href },
|
|
145
|
+
[`${SCHEMA}isBasedOn`]: { "@id": `${articleId.href}/revisions/2` },
|
|
146
|
+
[`${AS}#url`]: [{ "@id": `${articleId.href}/zh-Hant` }, {
|
|
147
|
+
"@type": `${AS}#Link`,
|
|
148
|
+
[`${AS}#href`]: { "@id": `${articleId.href}/zh-Hant.html` }
|
|
149
|
+
}]
|
|
150
|
+
};
|
|
151
|
+
const t = await Translation.fromJsonLd(expanded, { documentLoader: noFetch });
|
|
152
|
+
deepStrictEqual(t.language?.baseName, "zh-Hant");
|
|
153
|
+
deepStrictEqual(t.original, articleId);
|
|
154
|
+
deepStrictEqual(t.basis?.href, `${articleId.href}/revisions/2`);
|
|
155
|
+
deepStrictEqual(t.sourceUpdated, null);
|
|
156
|
+
ok(t.urls[1] instanceof Link);
|
|
157
|
+
const compact = record(await t.toJsonLd({ format: "compact" }));
|
|
158
|
+
deepStrictEqual(compact.translator, [alice.href]);
|
|
159
|
+
deepStrictEqual(compact.inLanguage, "zh-Hant");
|
|
160
|
+
deepStrictEqual(compact.isBasedOn, `${articleId.href}/revisions/2`);
|
|
161
|
+
ok(!("sourceUpdated" in compact));
|
|
162
|
+
ok(Object.keys(compact).every((key) => !key.startsWith("https:")));
|
|
163
|
+
});
|
|
164
|
+
test("FEP-22cd organization languages, multiple translators and review-only Update", async () => {
|
|
165
|
+
const initial = article().clone({
|
|
166
|
+
attribution: new URL("https://example.com/orgs/acme"),
|
|
167
|
+
names: [new LanguageString$1("Title", "en"), new LanguageString$1("제목", "ko")],
|
|
168
|
+
summaries: [new LanguageString$1("Summary", "en"), new LanguageString$1("요약", "ko")],
|
|
169
|
+
contents: [
|
|
170
|
+
...article().contents,
|
|
171
|
+
new LanguageString$1("日本語版", "ja"),
|
|
172
|
+
"Fallback"
|
|
173
|
+
],
|
|
174
|
+
translations: [translation(), new Translation({
|
|
175
|
+
language: new Intl.Locale("ja"),
|
|
176
|
+
translators: [new URL("https://example.com/users/bob"), new URL("https://example.com/actors/translator")],
|
|
177
|
+
original: articleId
|
|
178
|
+
})]
|
|
179
|
+
});
|
|
180
|
+
const editedAt = TemporalPolyfill.Instant.from("2026-09-15T00:00:00Z");
|
|
181
|
+
const edited = initial.clone({ updated: editedAt });
|
|
182
|
+
ok(TemporalPolyfill.Instant.compare(edited.translations[0].sourceUpdated, edited.updated) < 0);
|
|
183
|
+
const reviewed = edited.clone({ translations: [edited.translations[0].clone({ sourceUpdated: edited.updated }), edited.translations[1]] });
|
|
184
|
+
const update = new Update({
|
|
185
|
+
id: new URL(`${articleId.href}/review`),
|
|
186
|
+
actor: alice,
|
|
187
|
+
object: reviewed
|
|
188
|
+
});
|
|
189
|
+
const object = await (await Update.fromJsonLd(await update.toJsonLd({ format: "compact" }), { documentLoader: noFetch })).getObject();
|
|
190
|
+
ok(object instanceof Article);
|
|
191
|
+
deepStrictEqual(object.id, articleId);
|
|
192
|
+
const byLanguage = (values) => Object.fromEntries(values.map((value) => [value instanceof LanguageString$1 ? value.locale.baseName : "@none", value.toString()]));
|
|
193
|
+
deepStrictEqual(byLanguage(object.contents), byLanguage(initial.contents));
|
|
194
|
+
deepStrictEqual(object.names, initial.names);
|
|
195
|
+
deepStrictEqual(object.summaries, initial.summaries);
|
|
196
|
+
deepStrictEqual(object.attributionId, initial.attributionId);
|
|
197
|
+
deepStrictEqual(object.translations.map((t) => t.language?.baseName), ["ko", "ja"]);
|
|
198
|
+
deepStrictEqual(object.translations[0].sourceUpdated?.toString(), editedAt.toString());
|
|
199
|
+
deepStrictEqual(object.translations[1].sourceUpdated, null);
|
|
200
|
+
deepStrictEqual(object.translations[1].translatorIds, initial.translations[1].translatorIds);
|
|
201
|
+
const withdrawn = reviewed.clone({
|
|
202
|
+
translations: [],
|
|
203
|
+
contents: [new LanguageString$1("Original", "en")]
|
|
204
|
+
});
|
|
205
|
+
const replaced = await Article.fromJsonLd(await withdrawn.toJsonLd());
|
|
206
|
+
deepStrictEqual(replaced.translations, []);
|
|
207
|
+
deepStrictEqual(replaced.id, articleId);
|
|
208
|
+
});
|
|
209
|
+
test("FEP-22cd nested fresh and cached objects compact without full-IRI terms", async () => {
|
|
210
|
+
for (const child of [article(), await Article.fromJsonLd(example)]) for (const wrapper of [new Create({ object: child }), new Collection({ items: [child] })]) for (const format of [void 0, "compact"]) {
|
|
211
|
+
const json = record(await wrapper.toJsonLd({ format }));
|
|
212
|
+
deepStrictEqual(record(json.object ?? json.items).translations, [entry]);
|
|
213
|
+
const roundTrip = await Object$1.fromJsonLd(json);
|
|
214
|
+
ok(roundTrip instanceof Create || roundTrip instanceof Collection);
|
|
215
|
+
}
|
|
216
|
+
const custom = record(await article().toJsonLd({
|
|
217
|
+
format: "compact",
|
|
218
|
+
context: AS
|
|
219
|
+
}));
|
|
220
|
+
deepStrictEqual(custom["@context"], AS);
|
|
221
|
+
ok(`${FEP}#translations` in custom);
|
|
222
|
+
});
|
|
223
|
+
test("FEP-22cd all actor types use the existing linked-object getters", async () => {
|
|
224
|
+
for (const Type of [
|
|
225
|
+
Person,
|
|
226
|
+
Organization,
|
|
227
|
+
Group,
|
|
228
|
+
Application,
|
|
229
|
+
Service
|
|
230
|
+
]) {
|
|
231
|
+
const t = new Translation({ translator: new Type({ id: alice }) });
|
|
232
|
+
ok(await t.getTranslator({ documentLoader: noFetch }) instanceof Type);
|
|
233
|
+
const parsed = await Translation.fromJsonLd(await t.toJsonLd());
|
|
234
|
+
ok(await parsed.getTranslator({ documentLoader: actorLoader(Type.name) }) instanceof Type);
|
|
235
|
+
deepStrictEqual((await Array.fromAsync(parsed.getTranslators())).length, 1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
test("FEP-22cd metadata ids are not fetched or trusted as actor origins", async () => {
|
|
239
|
+
for (const id of [void 0, "https://victim.example/metadata"]) for (const plural of [false, true]) {
|
|
240
|
+
const t = await Translation.fromJsonLd({
|
|
241
|
+
"@context": context,
|
|
242
|
+
type: "Translation",
|
|
243
|
+
...id == null ? {} : { id },
|
|
244
|
+
translator: [{
|
|
245
|
+
id: "https://victim.example/actor",
|
|
246
|
+
type: "Application"
|
|
247
|
+
}]
|
|
248
|
+
});
|
|
249
|
+
const fetched = [];
|
|
250
|
+
const documentLoader = (url) => {
|
|
251
|
+
fetched.push(url);
|
|
252
|
+
return actorLoader("Person")(url);
|
|
253
|
+
};
|
|
254
|
+
const actors = plural ? await Array.fromAsync(t.getTranslators({ documentLoader })) : [await t.getTranslator({ documentLoader })];
|
|
255
|
+
deepStrictEqual(fetched, ["https://victim.example/actor"]);
|
|
256
|
+
ok(actors[0] instanceof Person);
|
|
257
|
+
deepStrictEqual(t.id?.href, id);
|
|
258
|
+
}
|
|
259
|
+
const parsed = await Article.fromJsonLd({
|
|
260
|
+
...example,
|
|
261
|
+
translations: [{
|
|
262
|
+
...entry,
|
|
263
|
+
id: "urn:translation:ko"
|
|
264
|
+
}]
|
|
265
|
+
}, { documentLoader: noFetch });
|
|
266
|
+
deepStrictEqual(parsed.translations[0].id?.href, "urn:translation:ko");
|
|
267
|
+
await parsed.toJsonLd({ format: "compact" });
|
|
268
|
+
});
|
|
269
|
+
async function hasCompleteCredit(t, documentLoader) {
|
|
270
|
+
const ids = t.translatorIds;
|
|
271
|
+
const actors = await Array.fromAsync(t.getTranslators({
|
|
272
|
+
documentLoader,
|
|
273
|
+
suppressError: true
|
|
274
|
+
}));
|
|
275
|
+
return actors.length > 0 && actors.length >= ids.length && actors.every((actor) => actor.id != null);
|
|
276
|
+
}
|
|
277
|
+
test("FEP-22cd unavailable, deleted, mismatched and id-less actors leave status unknown", async () => {
|
|
278
|
+
for (const failedLoader of [
|
|
279
|
+
noFetch,
|
|
280
|
+
actorLoader("Tombstone"),
|
|
281
|
+
actorLoader("Person", "https://spoof.example/actor")
|
|
282
|
+
]) {
|
|
283
|
+
const t = new Translation({ translators: [alice, new Application({ id: new URL("https://example.com/machine") })] });
|
|
284
|
+
deepStrictEqual(await hasCompleteCredit(t, failedLoader), false);
|
|
285
|
+
deepStrictEqual(t.translatorIds, [alice, new URL("https://example.com/machine")]);
|
|
286
|
+
deepStrictEqual(record(await t.toJsonLd({ format: "compact" })).translator instanceof Array, true);
|
|
287
|
+
}
|
|
288
|
+
deepStrictEqual(await hasCompleteCredit(await Translation.fromJsonLd({
|
|
289
|
+
"@context": context,
|
|
290
|
+
type: "Translation",
|
|
291
|
+
translator: [alice.href, { type: "Application" }]
|
|
292
|
+
}), actorLoader("Person")), false);
|
|
293
|
+
await rejects(() => new Translation({ translator: alice }).getTranslator({ documentLoader: noFetch }), /Unexpected fetch/);
|
|
294
|
+
const t = new Translation({ translator: alice });
|
|
295
|
+
await rejects(() => t.getTranslator({
|
|
296
|
+
documentLoader: actorLoader("Person", "https://spoof.example/actor"),
|
|
297
|
+
crossOrigin: "throw"
|
|
298
|
+
}));
|
|
299
|
+
});
|
|
300
|
+
test("FEP-22cd malformed metadata follows vocabulary parsing behavior", async () => {
|
|
301
|
+
await rejects(() => Article.fromJsonLd({
|
|
302
|
+
...example,
|
|
303
|
+
translations: [{
|
|
304
|
+
...entry,
|
|
305
|
+
inLanguage: "not_a_language"
|
|
306
|
+
}]
|
|
307
|
+
}), RangeError);
|
|
308
|
+
await rejects(() => Article.fromJsonLd({
|
|
309
|
+
...example,
|
|
310
|
+
translations: [{
|
|
311
|
+
...entry,
|
|
312
|
+
sourceUpdated: "2026-09-01"
|
|
313
|
+
}]
|
|
314
|
+
}), RangeError);
|
|
315
|
+
await rejects(() => Article.fromJsonLd({
|
|
316
|
+
...example,
|
|
317
|
+
translations: [{
|
|
318
|
+
...entry,
|
|
319
|
+
type: "Note"
|
|
320
|
+
}]
|
|
321
|
+
}), TypeError);
|
|
322
|
+
const idOnly = await Article.fromJsonLd({
|
|
323
|
+
...example,
|
|
324
|
+
translations: [{ id: "urn:translation:ko" }]
|
|
325
|
+
}, { documentLoader: noFetch });
|
|
326
|
+
deepStrictEqual(idOnly.translations[0].id?.href, "urn:translation:ko");
|
|
327
|
+
deepStrictEqual(idOnly.translations[0].language, null);
|
|
328
|
+
deepStrictEqual(idOnly.translations[0].translatorIds, []);
|
|
329
|
+
deepStrictEqual((await Article.fromJsonLd({
|
|
330
|
+
...example,
|
|
331
|
+
translations: void 0
|
|
332
|
+
})).translations, []);
|
|
333
|
+
});
|
|
334
|
+
test("FEP-22cd ordinary multilingual serialization remains unchanged", async () => {
|
|
335
|
+
const fields = {
|
|
336
|
+
id: articleId,
|
|
337
|
+
attribution: alice,
|
|
338
|
+
contents: [
|
|
339
|
+
"Fallback",
|
|
340
|
+
new LanguageString$1("Original", "en"),
|
|
341
|
+
new LanguageString$1("번역", "ko")
|
|
342
|
+
],
|
|
343
|
+
names: [new LanguageString$1("Title", "en"), new LanguageString$1("제목", "ko")]
|
|
344
|
+
};
|
|
345
|
+
for (const Type of [
|
|
346
|
+
Article,
|
|
347
|
+
Note,
|
|
348
|
+
Object$1
|
|
349
|
+
]) for (const format of [void 0, "compact"]) {
|
|
350
|
+
const json = record(await new Type(fields).toJsonLd({ format }));
|
|
351
|
+
ok(!JSON.stringify(json["@context"]).includes(FEP));
|
|
352
|
+
deepStrictEqual(json.contentMap, {
|
|
353
|
+
en: "Original",
|
|
354
|
+
ko: "번역"
|
|
355
|
+
});
|
|
356
|
+
deepStrictEqual(json.content, "Fallback");
|
|
357
|
+
deepStrictEqual(json.nameMap, {
|
|
358
|
+
en: "Title",
|
|
359
|
+
ko: "제목"
|
|
360
|
+
});
|
|
361
|
+
deepStrictEqual(json.attributedTo, alice.href);
|
|
362
|
+
ok(!("translations" in json));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
test("FEP-22cd keeps Mastodon PropertyValue and Schema.org translation namespaces distinct", async () => {
|
|
366
|
+
const json = record(await new Person({
|
|
367
|
+
id: alice,
|
|
368
|
+
attachments: [new PropertyValue({
|
|
369
|
+
name: "Website",
|
|
370
|
+
value: "https://example.com/"
|
|
371
|
+
})],
|
|
372
|
+
translations: [translation()]
|
|
373
|
+
}).toJsonLd());
|
|
374
|
+
deepStrictEqual(json.attachment, {
|
|
375
|
+
type: "PropertyValue",
|
|
376
|
+
name: "Website",
|
|
377
|
+
value: "https://example.com/"
|
|
378
|
+
});
|
|
379
|
+
deepStrictEqual(json.translations, [entry]);
|
|
380
|
+
const restored = await Person.fromJsonLd(json);
|
|
381
|
+
const attachments = await Array.fromAsync(restored.getAttachments());
|
|
382
|
+
ok(attachments[0] instanceof PropertyValue);
|
|
383
|
+
deepStrictEqual(attachments[0].value, "https://example.com/");
|
|
384
|
+
deepStrictEqual(restored.translations[0].original, articleId);
|
|
385
|
+
});
|
|
386
|
+
test("FEP-22cd explicit actor trust remains opt-in and clones retain untrusted state", async () => {
|
|
387
|
+
const parsed = await Translation.fromJsonLd({
|
|
388
|
+
"@context": context,
|
|
389
|
+
type: "Translation",
|
|
390
|
+
id: "https://victim.example/metadata",
|
|
391
|
+
translator: [{
|
|
392
|
+
id: "https://victim.example/actor",
|
|
393
|
+
type: "Application"
|
|
394
|
+
}]
|
|
395
|
+
});
|
|
396
|
+
ok(await parsed.getTranslator({
|
|
397
|
+
documentLoader: noFetch,
|
|
398
|
+
crossOrigin: "trust"
|
|
399
|
+
}) instanceof Application);
|
|
400
|
+
ok(await parsed.clone({ sourceUpdated }).getTranslator({ documentLoader: actorLoader("Person") }) instanceof Person);
|
|
401
|
+
ok(await parsed.clone({ translator: new Application({ id: alice }) }).getTranslator({ documentLoader: noFetch }) instanceof Application);
|
|
402
|
+
});
|
|
403
|
+
//#endregion
|
|
404
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
$schema: ../../vocab-tools/schema.yaml
|
|
2
|
+
name: Translation
|
|
3
|
+
compactName: Translation
|
|
4
|
+
uri: "https://w3id.org/fep/22cd#Translation"
|
|
5
|
+
entity: false
|
|
6
|
+
trustEmbeddedObjects: false
|
|
7
|
+
description: |
|
|
8
|
+
Translator credit and source freshness for one language version of an
|
|
9
|
+
{@link Object}, as defined by the draft
|
|
10
|
+
[FEP-22cd](https://w3id.org/fep/22cd). This is embedded metadata, not an
|
|
11
|
+
independent post or activity target. Its optional identifier need not be
|
|
12
|
+
dereferenceable and does not establish trust in embedded translator actors.
|
|
13
|
+
|
|
14
|
+
Targets [draft revision 6d0d6559](https://codeberg.org/fediverse/fep/src/commit/6d0d6559054baeb7b71a5f2bdc14fc4c09b66f39/fep/22cd/fep-22cd.md).
|
|
15
|
+
defaultContext:
|
|
16
|
+
- "https://www.w3.org/ns/activitystreams"
|
|
17
|
+
- "https://w3id.org/fep/22cd"
|
|
18
|
+
properties:
|
|
19
|
+
- singularName: language
|
|
20
|
+
functional: true
|
|
21
|
+
compactName: inLanguage
|
|
22
|
+
uri: "https://schema.org/inLanguage"
|
|
23
|
+
description: |
|
|
24
|
+
The translated language. Publishers must match a language in the
|
|
25
|
+
containing object's content, name, or summary map.
|
|
26
|
+
range:
|
|
27
|
+
- "fedify:langTag"
|
|
28
|
+
|
|
29
|
+
- pluralName: translators
|
|
30
|
+
singularName: translator
|
|
31
|
+
singularAccessor: true
|
|
32
|
+
compactName: translator
|
|
33
|
+
uri: "https://schema.org/translator"
|
|
34
|
+
extraContext: "https://w3id.org/fep/22cd"
|
|
35
|
+
description: |
|
|
36
|
+
The actors credited for this language version. Their order has no
|
|
37
|
+
meaning. Human actors credit translation or review; Application and
|
|
38
|
+
Service actors credit machine output. If any actor cannot be resolved,
|
|
39
|
+
the entry's human-or-machine status is unknown. Credit is asserted by
|
|
40
|
+
the publisher and grants no publishing authority. Embedded actors with
|
|
41
|
+
identifiers are fetched before being returned unless explicitly trusted.
|
|
42
|
+
range:
|
|
43
|
+
- "https://www.w3.org/ns/activitystreams#Application"
|
|
44
|
+
- "https://www.w3.org/ns/activitystreams#Group"
|
|
45
|
+
- "https://www.w3.org/ns/activitystreams#Organization"
|
|
46
|
+
- "https://www.w3.org/ns/activitystreams#Person"
|
|
47
|
+
- "https://www.w3.org/ns/activitystreams#Service"
|
|
48
|
+
|
|
49
|
+
- singularName: original
|
|
50
|
+
functional: true
|
|
51
|
+
compactName: translationOfWork
|
|
52
|
+
uri: "https://schema.org/translationOfWork"
|
|
53
|
+
description: |
|
|
54
|
+
The identifier of the containing object. FEP-22cd translations share
|
|
55
|
+
that object's identity rather than introducing separate posts.
|
|
56
|
+
range:
|
|
57
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
58
|
+
|
|
59
|
+
- singularName: sourceUpdated
|
|
60
|
+
functional: true
|
|
61
|
+
compactName: sourceUpdated
|
|
62
|
+
uri: "https://w3id.org/fep/22cd#sourceUpdated"
|
|
63
|
+
description: |
|
|
64
|
+
The source object's updated timestamp (or published timestamp if it has
|
|
65
|
+
no updated timestamp) at the last human review of this translation.
|
|
66
|
+
An absent value makes no freshness claim. A value earlier than the
|
|
67
|
+
current source timestamp indicates a potentially stale translation;
|
|
68
|
+
an equal or later value reflects the current source.
|
|
69
|
+
range:
|
|
70
|
+
- "http://www.w3.org/2001/XMLSchema#dateTime"
|
|
71
|
+
|
|
72
|
+
- singularName: basis
|
|
73
|
+
functional: true
|
|
74
|
+
compactName: isBasedOn
|
|
75
|
+
uri: "https://schema.org/isBasedOn"
|
|
76
|
+
description: |
|
|
77
|
+
An optional public revision resource identifying the source reviewed.
|
|
78
|
+
Supplements sourceUpdated without requiring private revision history
|
|
79
|
+
to be exposed.
|
|
80
|
+
range:
|
|
81
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
82
|
+
|
|
83
|
+
- pluralName: urls
|
|
84
|
+
singularName: url
|
|
85
|
+
singularAccessor: true
|
|
86
|
+
compactName: url
|
|
87
|
+
uri: "https://www.w3.org/ns/activitystreams#url"
|
|
88
|
+
description: Links to renderings of this specific language version.
|
|
89
|
+
range:
|
|
90
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
91
|
+
- "https://www.w3.org/ns/activitystreams#Link"
|
package/dist-tests/type.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
|
-
import {
|
|
4
|
-
import { t as getTypeId } from "./type-
|
|
3
|
+
import { E as Link, F as Person, J as getEntityTypeById, Y as isEntityType } from "./vocab-DaniQwU7.mjs";
|
|
4
|
+
import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
|
|
5
5
|
import { test } from "@fedify/fixture";
|
|
6
6
|
import { deepStrictEqual } from "node:assert/strict";
|
|
7
7
|
//#region src/type.test.ts
|