@fedify/vocab 2.4.0-dev.1962 → 2.4.0-dev.1964

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.
@@ -0,0 +1,264 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ globalThis.addEventListener = () => {};
3
+ import { k as Object$1 } from "./vocab-D3s0_CFf.mjs";
4
+ import { i as isActor, o as name, s as version } from "./actor-Bms47rRP.mjs";
5
+ import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
6
+ import { lookupWebFinger } from "@fedify/webfinger";
7
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
8
+ import { getLogger } from "@logtape/logtape";
9
+ import { getDocumentLoader, haveSameFe34Origin, haveSameIriOrigin, parseIri } from "@fedify/vocab-runtime";
10
+ import { delay } from "es-toolkit";
11
+ //#region src/handle.ts
12
+ /**
13
+ * Regular expression to match a fediverse handle in the format `@user@server`
14
+ * or `user@server`. The `user` part can contain alphanumeric characters and
15
+ * some special characters except `@`. The `server` part is all characters
16
+ * after the `@` symbol in the middle.
17
+ */
18
+ const handleRegexp = /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/;
19
+ /**
20
+ * Parses a fediverse handle in the format `@user@server` or `user@server`.
21
+ * The `user` part can contain alphanumeric characters and some special
22
+ * characters except `@`. The `server` part is all characters after the `@`
23
+ * symbol in the middle.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const handle = parseFediverseHandle("@username@example.com");
28
+ * console.log(handle?.username); // "username"
29
+ * console.log(handle?.host); // "example.com"
30
+ * ```
31
+ *
32
+ * @param handle - The fediverse handle string to parse.
33
+ * @returns A {@link FediverseHandle} object with `username` and `host`
34
+ * if the input is valid; otherwise `null`.
35
+ * @since 1.8.0
36
+ */
37
+ function parseFediverseHandle(handle) {
38
+ const match = handleRegexp.exec(handle);
39
+ if (match) return {
40
+ username: match[1],
41
+ host: match[2]
42
+ };
43
+ return null;
44
+ }
45
+ /**
46
+ * Converts a fediverse handle in the format `@user@server` or `user@server`
47
+ * to an `acct:` URI, which is a URL-like identifier for ActivityPub actors.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const identifier = toAcctUrl("@username@example.com");
52
+ * console.log(identifier?.href); // "acct:username@example.com"
53
+ * ```
54
+ *
55
+ * @param handle - The fediverse handle string to convert.
56
+ * @returns A `URL` object representing the `acct:` URI if conversion succeeds;
57
+ * otherwise `null`.
58
+ * @since 1.8.0
59
+ */
60
+ function toAcctUrl(handle) {
61
+ const parsed = parseFediverseHandle(handle);
62
+ if (!parsed) return null;
63
+ return new URL(`acct:${parsed.username}@${parsed.host}`);
64
+ }
65
+ //#endregion
66
+ //#region src/lookup.ts
67
+ const objectLookupCounters = /* @__PURE__ */ new WeakMap();
68
+ function getObjectLookupCounter(meterProvider) {
69
+ let counter = objectLookupCounters.get(meterProvider);
70
+ if (counter == null) {
71
+ counter = meterProvider.getMeter(name, version).createCounter("activitypub.object.lookup", {
72
+ description: "ActivityStreams object lookups via lookupObject(), classified by whether the resolved value is an Actor.",
73
+ unit: "{lookup}"
74
+ });
75
+ objectLookupCounters.set(meterProvider, counter);
76
+ }
77
+ return counter;
78
+ }
79
+ function getLookupRemoteHost(identifier) {
80
+ let url;
81
+ if (identifier instanceof URL) url = identifier;
82
+ else try {
83
+ url = new URL(identifier);
84
+ } catch {
85
+ return extractHandleHost(identifier.startsWith("@") ? identifier.slice(1) : identifier);
86
+ }
87
+ if (url.host !== "") return url.host;
88
+ if (url.protocol === "acct:") return extractHandleHost(url.pathname);
89
+ }
90
+ function extractHandleHost(handle) {
91
+ const at = handle.lastIndexOf("@");
92
+ if (at < 0 || at >= handle.length - 1) return void 0;
93
+ const candidate = handle.slice(at + 1);
94
+ if (/[/?#\s]/.test(candidate)) return void 0;
95
+ try {
96
+ return new URL(`https://${candidate}`).host || void 0;
97
+ } catch {
98
+ return;
99
+ }
100
+ }
101
+ const logger = getLogger([
102
+ "fedify",
103
+ "vocab",
104
+ "lookup"
105
+ ]);
106
+ /**
107
+ * Looks up an ActivityStreams object by its URI (including `acct:` URIs)
108
+ * or a fediverse handle (e.g., `@user@server` or `user@server`).
109
+ *
110
+ * @example
111
+ * ``` typescript
112
+ * // Look up an actor by its fediverse handle:
113
+ * await lookupObject("@hongminhee@fosstodon.org");
114
+ * // returning a `Person` object.
115
+ *
116
+ * // A fediverse handle can omit the leading '@':
117
+ * await lookupObject("hongminhee@fosstodon.org");
118
+ * // returning a `Person` object.
119
+ *
120
+ * // A `acct:` URI can be used as well:
121
+ * await lookupObject("acct:hongminhee@fosstodon.org");
122
+ * // returning a `Person` object.
123
+ *
124
+ * // Look up an object by its URI:
125
+ * await lookupObject("https://todon.eu/@hongminhee/112060633798771581");
126
+ * // returning a `Note` object.
127
+ *
128
+ * // It can be a `URL` object as well:
129
+ * await lookupObject(new URL("https://todon.eu/@hongminhee/112060633798771581"));
130
+ * // returning a `Note` object.
131
+ * ```
132
+ *
133
+ * @param identifier The URI or fediverse handle to look up.
134
+ * @param options Lookup options.
135
+ * @returns The object, or `null` if not found.
136
+ * @since 0.2.0
137
+ */
138
+ async function lookupObject(identifier, options = {}) {
139
+ return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
140
+ let kind = "other";
141
+ try {
142
+ const result = await lookupObjectInternal(identifier, options);
143
+ if (result != null) kind = isActor(result) ? "actor" : "object";
144
+ if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
145
+ else {
146
+ if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
147
+ span.setAttribute("activitypub.object.type", getTypeId(result).href);
148
+ if (result.replyTargetIds.length > 0) span.setAttribute("activitypub.object.in_reply_to", result.replyTargetIds.map((id) => id.href));
149
+ span.addEvent("activitypub.object.fetched", {
150
+ "activitypub.object.type": getTypeId(result).href,
151
+ "activitypub.object.json": JSON.stringify(await result.toJsonLd(options))
152
+ });
153
+ }
154
+ return result;
155
+ } catch (error) {
156
+ span.recordException(error);
157
+ span.setStatus({
158
+ code: SpanStatusCode.ERROR,
159
+ message: String(error)
160
+ });
161
+ throw error;
162
+ } finally {
163
+ if (options.meterProvider != null) {
164
+ const attributes = { "activitypub.lookup.kind": kind };
165
+ const host = getLookupRemoteHost(identifier);
166
+ if (host != null) attributes["activitypub.remote.host"] = host;
167
+ getObjectLookupCounter(options.meterProvider).add(1, attributes);
168
+ }
169
+ span.end();
170
+ }
171
+ });
172
+ }
173
+ async function lookupObjectInternal(identifier, options = {}) {
174
+ const documentLoader = options.documentLoader ?? getDocumentLoader({ userAgent: options.userAgent });
175
+ if (typeof identifier === "string") identifier = toAcctUrl(identifier) ?? new URL(identifier);
176
+ let remoteDoc = null;
177
+ if (identifier.protocol === "http:" || identifier.protocol === "https:") try {
178
+ remoteDoc = await documentLoader(identifier.href, { signal: options.signal });
179
+ } catch (error) {
180
+ logger.debug("Failed to fetch remote document:\n{error}", { error });
181
+ }
182
+ if (remoteDoc == null) {
183
+ const jrd = await lookupWebFinger(identifier, {
184
+ userAgent: options.userAgent,
185
+ tracerProvider: options.tracerProvider,
186
+ meterProvider: options.meterProvider,
187
+ allowPrivateAddress: "allowPrivateAddress" in options && options.allowPrivateAddress === true,
188
+ signal: options.signal
189
+ });
190
+ if (jrd?.links == null) return null;
191
+ for (const l of jrd.links) {
192
+ if (l.type !== "application/activity+json" && !l.type?.match(/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/) || l.rel !== "self" || l.href == null) continue;
193
+ try {
194
+ remoteDoc = await documentLoader(l.href, { signal: options.signal });
195
+ break;
196
+ } catch (error) {
197
+ logger.debug("Failed to fetch remote document:\n{error}", { error });
198
+ continue;
199
+ }
200
+ }
201
+ }
202
+ if (remoteDoc == null) return null;
203
+ let object;
204
+ let documentUrl;
205
+ try {
206
+ documentUrl = parseIri(remoteDoc.documentUrl);
207
+ object = await Object$1.fromJsonLd(remoteDoc.document, {
208
+ documentLoader,
209
+ contextLoader: options.contextLoader,
210
+ tracerProvider: options.tracerProvider,
211
+ baseUrl: documentUrl
212
+ });
213
+ } catch (error) {
214
+ if (error instanceof TypeError) {
215
+ logger.debug("Failed to parse JSON-LD document: {error}\n{document}", {
216
+ ...remoteDoc,
217
+ error
218
+ });
219
+ return null;
220
+ }
221
+ throw error;
222
+ }
223
+ if (options.crossOrigin !== "trust" && object.id != null && !haveSameIriOrigin(object.id, documentUrl) && !haveSameFe34Origin(object.id, documentUrl)) {
224
+ if (options.crossOrigin === "throw") throw new Error(`The object's @id (${object.id.href}) has a different origin than the document URL (${remoteDoc.documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to "trust".`);
225
+ logger.warn("The object's @id ({objectId}) has a different origin than the document URL ({documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to \"trust\".", {
226
+ ...remoteDoc,
227
+ objectId: object.id.href
228
+ });
229
+ return null;
230
+ }
231
+ return object;
232
+ }
233
+ /**
234
+ * Traverses a collection, yielding each item in the collection.
235
+ * If the collection is paginated, it will fetch the next page
236
+ * automatically.
237
+ *
238
+ * @example
239
+ * ``` typescript
240
+ * const collection = await lookupObject(collectionUrl);
241
+ * if (collection instanceof Collection) {
242
+ * for await (const item of traverseCollection(collection)) {
243
+ * console.log(item.id?.href);
244
+ * }
245
+ * }
246
+ * ```
247
+ *
248
+ * @param collection The collection to traverse.
249
+ * @param options Options for traversing the collection.
250
+ * @returns An async iterable of each item in the collection.
251
+ * @since 1.1.0
252
+ */
253
+ async function* traverseCollection(collection, options = {}) {
254
+ const interval = Temporal.Duration.from(options.interval ?? { seconds: 0 }).total("millisecond");
255
+ let page = await collection.getFirst(options);
256
+ if (page == null) for await (const item of collection.getItems(options)) yield item;
257
+ else while (page != null) {
258
+ for await (const item of page.getItems(options)) yield item;
259
+ if (interval > 0) await delay(interval);
260
+ page = await page.getNext(options);
261
+ }
262
+ }
263
+ //#endregion
264
+ export { traverseCollection as n, lookupObject as t };
@@ -1,269 +1,11 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { D as Note, M as Person, O as Object$1, o as Collection } from "./vocab-CNiWKo57.mjs";
4
- import { c as esm_default, i as isActor, o as name, s as version } from "./actor-DH8a9EGs.mjs";
5
- import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
3
+ import { N as Person, O as Note, k as Object$1, s as Collection } from "./vocab-D3s0_CFf.mjs";
4
+ import { t as esm_default } from "./esm-BcWVaclW.mjs";
5
+ import { n as traverseCollection, t as lookupObject } from "./lookup-DaLkzo2s.mjs";
6
6
  import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
7
7
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
8
8
  import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
9
- import { lookupWebFinger } from "@fedify/webfinger";
10
- import { SpanStatusCode, trace } from "@opentelemetry/api";
11
- import { getLogger } from "@logtape/logtape";
12
- import { getDocumentLoader, haveSameFe34Origin, haveSameIriOrigin, parseIri } from "@fedify/vocab-runtime";
13
- import { delay } from "es-toolkit";
14
- //#region src/handle.ts
15
- /**
16
- * Regular expression to match a fediverse handle in the format `@user@server`
17
- * or `user@server`. The `user` part can contain alphanumeric characters and
18
- * some special characters except `@`. The `server` part is all characters
19
- * after the `@` symbol in the middle.
20
- */
21
- const handleRegexp = /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/;
22
- /**
23
- * Parses a fediverse handle in the format `@user@server` or `user@server`.
24
- * The `user` part can contain alphanumeric characters and some special
25
- * characters except `@`. The `server` part is all characters after the `@`
26
- * symbol in the middle.
27
- *
28
- * @example
29
- * ```typescript
30
- * const handle = parseFediverseHandle("@username@example.com");
31
- * console.log(handle?.username); // "username"
32
- * console.log(handle?.host); // "example.com"
33
- * ```
34
- *
35
- * @param handle - The fediverse handle string to parse.
36
- * @returns A {@link FediverseHandle} object with `username` and `host`
37
- * if the input is valid; otherwise `null`.
38
- * @since 1.8.0
39
- */
40
- function parseFediverseHandle(handle) {
41
- const match = handleRegexp.exec(handle);
42
- if (match) return {
43
- username: match[1],
44
- host: match[2]
45
- };
46
- return null;
47
- }
48
- /**
49
- * Converts a fediverse handle in the format `@user@server` or `user@server`
50
- * to an `acct:` URI, which is a URL-like identifier for ActivityPub actors.
51
- *
52
- * @example
53
- * ```typescript
54
- * const identifier = toAcctUrl("@username@example.com");
55
- * console.log(identifier?.href); // "acct:username@example.com"
56
- * ```
57
- *
58
- * @param handle - The fediverse handle string to convert.
59
- * @returns A `URL` object representing the `acct:` URI if conversion succeeds;
60
- * otherwise `null`.
61
- * @since 1.8.0
62
- */
63
- function toAcctUrl(handle) {
64
- const parsed = parseFediverseHandle(handle);
65
- if (!parsed) return null;
66
- return new URL(`acct:${parsed.username}@${parsed.host}`);
67
- }
68
- //#endregion
69
- //#region src/lookup.ts
70
- const objectLookupCounters = /* @__PURE__ */ new WeakMap();
71
- function getObjectLookupCounter(meterProvider) {
72
- let counter = objectLookupCounters.get(meterProvider);
73
- if (counter == null) {
74
- counter = meterProvider.getMeter(name, version).createCounter("activitypub.object.lookup", {
75
- description: "ActivityStreams object lookups via lookupObject(), classified by whether the resolved value is an Actor.",
76
- unit: "{lookup}"
77
- });
78
- objectLookupCounters.set(meterProvider, counter);
79
- }
80
- return counter;
81
- }
82
- function getLookupRemoteHost(identifier) {
83
- let url;
84
- if (identifier instanceof URL) url = identifier;
85
- else try {
86
- url = new URL(identifier);
87
- } catch {
88
- return extractHandleHost(identifier.startsWith("@") ? identifier.slice(1) : identifier);
89
- }
90
- if (url.host !== "") return url.host;
91
- if (url.protocol === "acct:") return extractHandleHost(url.pathname);
92
- }
93
- function extractHandleHost(handle) {
94
- const at = handle.lastIndexOf("@");
95
- if (at < 0 || at >= handle.length - 1) return void 0;
96
- const candidate = handle.slice(at + 1);
97
- if (/[/?#\s]/.test(candidate)) return void 0;
98
- try {
99
- return new URL(`https://${candidate}`).host || void 0;
100
- } catch {
101
- return;
102
- }
103
- }
104
- const logger = getLogger([
105
- "fedify",
106
- "vocab",
107
- "lookup"
108
- ]);
109
- /**
110
- * Looks up an ActivityStreams object by its URI (including `acct:` URIs)
111
- * or a fediverse handle (e.g., `@user@server` or `user@server`).
112
- *
113
- * @example
114
- * ``` typescript
115
- * // Look up an actor by its fediverse handle:
116
- * await lookupObject("@hongminhee@fosstodon.org");
117
- * // returning a `Person` object.
118
- *
119
- * // A fediverse handle can omit the leading '@':
120
- * await lookupObject("hongminhee@fosstodon.org");
121
- * // returning a `Person` object.
122
- *
123
- * // A `acct:` URI can be used as well:
124
- * await lookupObject("acct:hongminhee@fosstodon.org");
125
- * // returning a `Person` object.
126
- *
127
- * // Look up an object by its URI:
128
- * await lookupObject("https://todon.eu/@hongminhee/112060633798771581");
129
- * // returning a `Note` object.
130
- *
131
- * // It can be a `URL` object as well:
132
- * await lookupObject(new URL("https://todon.eu/@hongminhee/112060633798771581"));
133
- * // returning a `Note` object.
134
- * ```
135
- *
136
- * @param identifier The URI or fediverse handle to look up.
137
- * @param options Lookup options.
138
- * @returns The object, or `null` if not found.
139
- * @since 0.2.0
140
- */
141
- async function lookupObject(identifier, options = {}) {
142
- return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
143
- let kind = "other";
144
- try {
145
- const result = await lookupObjectInternal(identifier, options);
146
- if (result != null) kind = isActor(result) ? "actor" : "object";
147
- if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
148
- else {
149
- if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
150
- span.setAttribute("activitypub.object.type", getTypeId(result).href);
151
- if (result.replyTargetIds.length > 0) span.setAttribute("activitypub.object.in_reply_to", result.replyTargetIds.map((id) => id.href));
152
- span.addEvent("activitypub.object.fetched", {
153
- "activitypub.object.type": getTypeId(result).href,
154
- "activitypub.object.json": JSON.stringify(await result.toJsonLd(options))
155
- });
156
- }
157
- return result;
158
- } catch (error) {
159
- span.recordException(error);
160
- span.setStatus({
161
- code: SpanStatusCode.ERROR,
162
- message: String(error)
163
- });
164
- throw error;
165
- } finally {
166
- if (options.meterProvider != null) {
167
- const attributes = { "activitypub.lookup.kind": kind };
168
- const host = getLookupRemoteHost(identifier);
169
- if (host != null) attributes["activitypub.remote.host"] = host;
170
- getObjectLookupCounter(options.meterProvider).add(1, attributes);
171
- }
172
- span.end();
173
- }
174
- });
175
- }
176
- async function lookupObjectInternal(identifier, options = {}) {
177
- const documentLoader = options.documentLoader ?? getDocumentLoader({ userAgent: options.userAgent });
178
- if (typeof identifier === "string") identifier = toAcctUrl(identifier) ?? new URL(identifier);
179
- let remoteDoc = null;
180
- if (identifier.protocol === "http:" || identifier.protocol === "https:") try {
181
- remoteDoc = await documentLoader(identifier.href, { signal: options.signal });
182
- } catch (error) {
183
- logger.debug("Failed to fetch remote document:\n{error}", { error });
184
- }
185
- if (remoteDoc == null) {
186
- const jrd = await lookupWebFinger(identifier, {
187
- userAgent: options.userAgent,
188
- tracerProvider: options.tracerProvider,
189
- meterProvider: options.meterProvider,
190
- allowPrivateAddress: "allowPrivateAddress" in options && options.allowPrivateAddress === true,
191
- signal: options.signal
192
- });
193
- if (jrd?.links == null) return null;
194
- for (const l of jrd.links) {
195
- if (l.type !== "application/activity+json" && !l.type?.match(/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/) || l.rel !== "self" || l.href == null) continue;
196
- try {
197
- remoteDoc = await documentLoader(l.href, { signal: options.signal });
198
- break;
199
- } catch (error) {
200
- logger.debug("Failed to fetch remote document:\n{error}", { error });
201
- continue;
202
- }
203
- }
204
- }
205
- if (remoteDoc == null) return null;
206
- let object;
207
- let documentUrl;
208
- try {
209
- documentUrl = parseIri(remoteDoc.documentUrl);
210
- object = await Object$1.fromJsonLd(remoteDoc.document, {
211
- documentLoader,
212
- contextLoader: options.contextLoader,
213
- tracerProvider: options.tracerProvider,
214
- baseUrl: documentUrl
215
- });
216
- } catch (error) {
217
- if (error instanceof TypeError) {
218
- logger.debug("Failed to parse JSON-LD document: {error}\n{document}", {
219
- ...remoteDoc,
220
- error
221
- });
222
- return null;
223
- }
224
- throw error;
225
- }
226
- if (options.crossOrigin !== "trust" && object.id != null && !haveSameIriOrigin(object.id, documentUrl) && !haveSameFe34Origin(object.id, documentUrl)) {
227
- if (options.crossOrigin === "throw") throw new Error(`The object's @id (${object.id.href}) has a different origin than the document URL (${remoteDoc.documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to "trust".`);
228
- logger.warn("The object's @id ({objectId}) has a different origin than the document URL ({documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to \"trust\".", {
229
- ...remoteDoc,
230
- objectId: object.id.href
231
- });
232
- return null;
233
- }
234
- return object;
235
- }
236
- /**
237
- * Traverses a collection, yielding each item in the collection.
238
- * If the collection is paginated, it will fetch the next page
239
- * automatically.
240
- *
241
- * @example
242
- * ``` typescript
243
- * const collection = await lookupObject(collectionUrl);
244
- * if (collection instanceof Collection) {
245
- * for await (const item of traverseCollection(collection)) {
246
- * console.log(item.id?.href);
247
- * }
248
- * }
249
- * ```
250
- *
251
- * @param collection The collection to traverse.
252
- * @param options Options for traversing the collection.
253
- * @returns An async iterable of each item in the collection.
254
- * @since 1.1.0
255
- */
256
- async function* traverseCollection(collection, options = {}) {
257
- const interval = Temporal.Duration.from(options.interval ?? { seconds: 0 }).total("millisecond");
258
- let page = await collection.getFirst(options);
259
- if (page == null) for await (const item of collection.getItems(options)) yield item;
260
- else while (page != null) {
261
- for await (const item of page.getItems(options)) yield item;
262
- if (interval > 0) await delay(interval);
263
- page = await page.getNext(options);
264
- }
265
- }
266
- //#endregion
267
9
  //#region src/lookup.test.ts
268
10
  test("lookupObject()", {
269
11
  sanitizeResources: false,
@@ -469,3 +469,18 @@ properties:
469
469
  (boost) was approved by the target post's author.
470
470
  range:
471
471
  - "https://gotosocial.org/ns#AnnounceAuthorization"
472
+
473
+ - pluralName: translations
474
+ singularName: translation
475
+ compactName: translations
476
+ uri: "https://w3id.org/fep/22cd#translations"
477
+ extraContext: "https://w3id.org/fep/22cd"
478
+ description: |
479
+ Translation metadata for the language versions in this object's content,
480
+ name, or summary maps, as defined by the draft
481
+ [FEP-22cd](https://w3id.org/fep/22cd). Each entry describes one translated
482
+ language; directly authored languages have no entry. The entries share
483
+ this object's identity, replies, and reactions. Translator credit grants
484
+ no authority to update the object.
485
+ range:
486
+ - "https://w3id.org/fep/22cd#Translation"