@fedify/vocab 2.4.0-pr.936.41 → 2.4.0

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.
Files changed (36) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +31328 -8723
  3. package/dist/mod.d.cts +4599 -6
  4. package/dist/mod.d.ts +4599 -6
  5. package/dist/mod.js +31221 -8623
  6. package/dist-tests/actor-D7_iX4I4.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/{actor-MBpzF3-D.mjs → esm-DP2Spt0u.mjs} +15 -211
  9. package/dist-tests/gateway.test.mjs +1906 -0
  10. package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
  11. package/dist-tests/lookup.test.mjs +10 -268
  12. package/dist-tests/object.yaml +15 -0
  13. package/dist-tests/portable-lookup.test.mjs +653 -0
  14. package/dist-tests/signed-representation.test.mjs +189 -0
  15. package/dist-tests/translation.test.mjs +404 -0
  16. package/dist-tests/translation.yaml +91 -0
  17. package/dist-tests/type.test.mjs +2 -2
  18. package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-COZcxf00.mjs} +30831 -8515
  19. package/dist-tests/vocab.test.mjs +677 -10
  20. package/package.json +5 -5
  21. package/src/__snapshots__/vocab.test.ts.snap +291 -15
  22. package/src/actor.test.ts +41 -40
  23. package/src/actor.ts +120 -1
  24. package/src/gateway.test.ts +2637 -0
  25. package/src/lookup.test.ts +6 -5
  26. package/src/lookup.ts +460 -33
  27. package/src/mod.ts +9 -0
  28. package/src/object.yaml +15 -0
  29. package/src/portable-lookup.test.ts +889 -0
  30. package/src/preprocessors.ts +1 -1
  31. package/src/signed-representation.test.ts +277 -0
  32. package/src/translation.test.ts +518 -0
  33. package/src/translation.yaml +91 -0
  34. package/src/vocab.test.ts +914 -5
  35. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  36. /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-COZcxf00.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-COZcxf00.mjs";
4
+ import "./actor-D7_iX4I4.mjs";
5
+ import "./lookup-IiNFWuXL.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"
@@ -1,7 +1,7 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { H as getEntityTypeById, M as Person, U as isEntityType, w as Link } from "./vocab-Bjwctrxn.mjs";
4
- import { t as getTypeId } from "./type-_BlsfT3S.mjs";
3
+ import { E as Link, F as Person, J as getEntityTypeById, Y as isEntityType } from "./vocab-COZcxf00.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