@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.
- package/deno.json +1 -1
- package/dist/mod.cjs +31328 -8723
- package/dist/mod.d.cts +4599 -6
- package/dist/mod.d.ts +4599 -6
- package/dist/mod.js +31221 -8623
- package/dist-tests/actor-D7_iX4I4.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-DP2Spt0u.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-IiNFWuXL.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-COZcxf00.mjs} +30831 -8515
- package/dist-tests/vocab.test.mjs +677 -10
- 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 +914 -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,480 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
globalThis.addEventListener = () => {};
|
|
3
|
+
import { A as Object$1 } from "./vocab-COZcxf00.mjs";
|
|
4
|
+
import { i as isActor, o as name, s as version } from "./actor-D7_iX4I4.mjs";
|
|
5
|
+
import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
|
|
6
|
+
import { canonicalizePortableUri, formatIri, getDocumentLoader, haveSameFe34Origin, haveSameIriOrigin, parseIri } from "@fedify/vocab-runtime";
|
|
7
|
+
import { lookupWebFinger } from "@fedify/webfinger";
|
|
8
|
+
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
9
|
+
import { getLogger } from "@logtape/logtape";
|
|
10
|
+
import { PortableObjectRejectedError, createSnapshotContextLoader, dereferencePortableIri, getPortableGatewayCandidates, getPortableResponseClaim, isPortableIri, parseCompatibleEf61Reference } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
11
|
+
import { delay } from "es-toolkit";
|
|
12
|
+
//#region src/handle.ts
|
|
13
|
+
/**
|
|
14
|
+
* Regular expression to match a fediverse handle in the format `@user@server`
|
|
15
|
+
* or `user@server`. The `user` part can contain alphanumeric characters and
|
|
16
|
+
* some special characters except `@`. The `server` part is all characters
|
|
17
|
+
* after the `@` symbol in the middle.
|
|
18
|
+
*/
|
|
19
|
+
const handleRegexp = /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/;
|
|
20
|
+
/**
|
|
21
|
+
* Parses a fediverse handle in the format `@user@server` or `user@server`.
|
|
22
|
+
* The `user` part can contain alphanumeric characters and some special
|
|
23
|
+
* characters except `@`. The `server` part is all characters after the `@`
|
|
24
|
+
* symbol in the middle.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const handle = parseFediverseHandle("@username@example.com");
|
|
29
|
+
* console.log(handle?.username); // "username"
|
|
30
|
+
* console.log(handle?.host); // "example.com"
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @param handle - The fediverse handle string to parse.
|
|
34
|
+
* @returns A {@link FediverseHandle} object with `username` and `host`
|
|
35
|
+
* if the input is valid; otherwise `null`.
|
|
36
|
+
* @since 1.8.0
|
|
37
|
+
*/
|
|
38
|
+
function parseFediverseHandle(handle) {
|
|
39
|
+
const match = handleRegexp.exec(handle);
|
|
40
|
+
if (match) return {
|
|
41
|
+
username: match[1],
|
|
42
|
+
host: match[2]
|
|
43
|
+
};
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Converts a fediverse handle in the format `@user@server` or `user@server`
|
|
48
|
+
* to an `acct:` URI, which is a URL-like identifier for ActivityPub actors.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const identifier = toAcctUrl("@username@example.com");
|
|
53
|
+
* console.log(identifier?.href); // "acct:username@example.com"
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @param handle - The fediverse handle string to convert.
|
|
57
|
+
* @returns A `URL` object representing the `acct:` URI if conversion succeeds;
|
|
58
|
+
* otherwise `null`.
|
|
59
|
+
* @since 1.8.0
|
|
60
|
+
*/
|
|
61
|
+
function toAcctUrl(handle) {
|
|
62
|
+
const parsed = parseFediverseHandle(handle);
|
|
63
|
+
if (!parsed) return null;
|
|
64
|
+
return new URL(`acct:${parsed.username}@${parsed.host}`);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/lookup.ts
|
|
68
|
+
const objectLookupCounters = /* @__PURE__ */ new WeakMap();
|
|
69
|
+
function getObjectLookupCounter(meterProvider) {
|
|
70
|
+
let counter = objectLookupCounters.get(meterProvider);
|
|
71
|
+
if (counter == null) {
|
|
72
|
+
counter = meterProvider.getMeter(name, version).createCounter("activitypub.object.lookup", {
|
|
73
|
+
description: "ActivityStreams object lookups via lookupObject(), classified by whether the resolved value is an Actor.",
|
|
74
|
+
unit: "{lookup}"
|
|
75
|
+
});
|
|
76
|
+
objectLookupCounters.set(meterProvider, counter);
|
|
77
|
+
}
|
|
78
|
+
return counter;
|
|
79
|
+
}
|
|
80
|
+
function getLookupRemoteHost(identifier) {
|
|
81
|
+
let url;
|
|
82
|
+
if (identifier instanceof URL) url = identifier;
|
|
83
|
+
else if (PORTABLE_IRI_PATTERN.test(identifier)) return;
|
|
84
|
+
else try {
|
|
85
|
+
url = new URL(identifier);
|
|
86
|
+
} catch {
|
|
87
|
+
return extractHandleHost(identifier.startsWith("@") ? identifier.slice(1) : identifier);
|
|
88
|
+
}
|
|
89
|
+
if (isPortableIri(url)) return void 0;
|
|
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
|
+
* The maximum number of requests to FEP-ef61 gateways that a single
|
|
111
|
+
* {@link lookupObject} call makes for portable objects. Gateways come from
|
|
112
|
+
* possibly untrusted WebFinger responses and location hints, so they are
|
|
113
|
+
* bounded to keep a single lookup from fanning out to many servers.
|
|
114
|
+
*/
|
|
115
|
+
const MAX_PORTABLE_ATTEMPTS = 5;
|
|
116
|
+
/**
|
|
117
|
+
* Looks up an ActivityStreams object by its URI (including `acct:` URIs)
|
|
118
|
+
* or a fediverse handle (e.g., `@user@server` or `user@server`).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ``` typescript
|
|
122
|
+
* // Look up an actor by its fediverse handle:
|
|
123
|
+
* await lookupObject("@hongminhee@fosstodon.org");
|
|
124
|
+
* // returning a `Person` object.
|
|
125
|
+
*
|
|
126
|
+
* // A fediverse handle can omit the leading '@':
|
|
127
|
+
* await lookupObject("hongminhee@fosstodon.org");
|
|
128
|
+
* // returning a `Person` object.
|
|
129
|
+
*
|
|
130
|
+
* // A `acct:` URI can be used as well:
|
|
131
|
+
* await lookupObject("acct:hongminhee@fosstodon.org");
|
|
132
|
+
* // returning a `Person` object.
|
|
133
|
+
*
|
|
134
|
+
* // Look up an object by its URI:
|
|
135
|
+
* await lookupObject("https://todon.eu/@hongminhee/112060633798771581");
|
|
136
|
+
* // returning a `Note` object.
|
|
137
|
+
*
|
|
138
|
+
* // It can be a `URL` object as well:
|
|
139
|
+
* await lookupObject(new URL("https://todon.eu/@hongminhee/112060633798771581"));
|
|
140
|
+
* // returning a `Note` object.
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* [FEP-ef61] portable objects, including portable actors found through
|
|
144
|
+
* WebFinger, are looked up only if the `verifyPortableObject` option is
|
|
145
|
+
* given; see {@link LookupObjectOptions.verifyPortableObject}. Pass
|
|
146
|
+
* {@link LookupObjectOptions.gateways} to look up a portable ID without
|
|
147
|
+
* `@gateway` location hints.
|
|
148
|
+
*
|
|
149
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
150
|
+
*
|
|
151
|
+
* @param identifier The URI or fediverse handle to look up.
|
|
152
|
+
* @param options Lookup options.
|
|
153
|
+
* @returns The object, or `null` if not found.
|
|
154
|
+
* @since 0.2.0
|
|
155
|
+
*/
|
|
156
|
+
async function lookupObject(identifier, options = {}) {
|
|
157
|
+
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
|
|
158
|
+
let kind = "other";
|
|
159
|
+
try {
|
|
160
|
+
const result = await lookupObjectInternal(identifier, options);
|
|
161
|
+
if (result != null) kind = isActor(result) ? "actor" : "object";
|
|
162
|
+
if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
163
|
+
else {
|
|
164
|
+
if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
|
|
165
|
+
span.setAttribute("activitypub.object.type", getTypeId(result).href);
|
|
166
|
+
if (result.replyTargetIds.length > 0) span.setAttribute("activitypub.object.in_reply_to", result.replyTargetIds.map((id) => id.href));
|
|
167
|
+
span.addEvent("activitypub.object.fetched", {
|
|
168
|
+
"activitypub.object.type": getTypeId(result).href,
|
|
169
|
+
"activitypub.object.json": JSON.stringify(await result.toJsonLd(options))
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
span.recordException(error);
|
|
175
|
+
span.setStatus({
|
|
176
|
+
code: SpanStatusCode.ERROR,
|
|
177
|
+
message: String(error)
|
|
178
|
+
});
|
|
179
|
+
throw error;
|
|
180
|
+
} finally {
|
|
181
|
+
if (options.meterProvider != null) {
|
|
182
|
+
const attributes = { "activitypub.lookup.kind": kind };
|
|
183
|
+
const host = getLookupRemoteHost(identifier);
|
|
184
|
+
if (host != null) attributes["activitypub.remote.host"] = host;
|
|
185
|
+
getObjectLookupCounter(options.meterProvider).add(1, attributes);
|
|
186
|
+
}
|
|
187
|
+
span.end();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function lookupObjectInternal(identifier, options = {}) {
|
|
192
|
+
const documentLoader = options.documentLoader ?? getDocumentLoader({ userAgent: options.userAgent });
|
|
193
|
+
const portable = {
|
|
194
|
+
options,
|
|
195
|
+
documentLoader,
|
|
196
|
+
attempted: /* @__PURE__ */ new Set(),
|
|
197
|
+
remaining: MAX_PORTABLE_ATTEMPTS
|
|
198
|
+
};
|
|
199
|
+
if (typeof identifier === "string") {
|
|
200
|
+
if (PORTABLE_IRI_PATTERN.test(identifier)) {
|
|
201
|
+
const candidate = parsePortableCandidate(identifier);
|
|
202
|
+
if (candidate == null) return null;
|
|
203
|
+
return await lookupPortableObject(portable, candidate, void 0);
|
|
204
|
+
}
|
|
205
|
+
if (options.verifyPortableObject != null && parseCompatibleEf61Reference(identifier) === null) return null;
|
|
206
|
+
identifier = toAcctUrl(identifier) ?? parseIri(identifier);
|
|
207
|
+
}
|
|
208
|
+
if (isPortableIri(identifier)) {
|
|
209
|
+
let candidate;
|
|
210
|
+
try {
|
|
211
|
+
candidate = parseIri(identifier);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof TypeError) return null;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
return await lookupPortableObject(portable, candidate, void 0);
|
|
217
|
+
}
|
|
218
|
+
let remoteDoc = null;
|
|
219
|
+
if (identifier.protocol === "http:" || identifier.protocol === "https:") {
|
|
220
|
+
const compatible = getCompatibleCandidate(identifier.href, options);
|
|
221
|
+
if (compatible !== void 0) {
|
|
222
|
+
if (compatible == null) return null;
|
|
223
|
+
return await lookupPortableObject(portable, compatible.id, [compatible.gateway]);
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
remoteDoc = await documentLoader(identifier.href, { signal: options.signal });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.debug("Failed to fetch remote document:\n{error}", { error });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (remoteDoc == null) {
|
|
232
|
+
const jrd = await lookupWebFinger(identifier, {
|
|
233
|
+
userAgent: options.userAgent,
|
|
234
|
+
tracerProvider: options.tracerProvider,
|
|
235
|
+
meterProvider: options.meterProvider,
|
|
236
|
+
allowPrivateAddress: "allowPrivateAddress" in options && options.allowPrivateAddress === true,
|
|
237
|
+
signal: options.signal
|
|
238
|
+
});
|
|
239
|
+
if (jrd?.links == null) return null;
|
|
240
|
+
const webFingerGateway = getWebFingerGateway(identifier);
|
|
241
|
+
for (const l of jrd.links) {
|
|
242
|
+
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;
|
|
243
|
+
if (PORTABLE_IRI_PATTERN.test(l.href)) {
|
|
244
|
+
if (options.verifyPortableObject == null) {
|
|
245
|
+
logger.debug("Skipping the portable self link {href}, as the verifyPortableObject option is not given.", { href: l.href });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const candidate = parsePortableCandidate(l.href);
|
|
249
|
+
if (candidate == null) continue;
|
|
250
|
+
const gateways = webFingerGateway == null ? [] : [webFingerGateway];
|
|
251
|
+
if (options.gateways == null) gateways.push(...getPortableGatewayCandidates(candidate));
|
|
252
|
+
const object = await lookupPortableObject(portable, candidate, gateways);
|
|
253
|
+
if (object != null) return object;
|
|
254
|
+
if (options.signal?.aborted) return null;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const compatible = getCompatibleCandidate(l.href, options);
|
|
258
|
+
if (compatible !== void 0) {
|
|
259
|
+
if (compatible == null) continue;
|
|
260
|
+
const object = await lookupPortableObject(portable, compatible.id, [compatible.gateway]);
|
|
261
|
+
if (object != null) return object;
|
|
262
|
+
if (options.signal?.aborted) return null;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
remoteDoc = await documentLoader(l.href, { signal: options.signal });
|
|
267
|
+
break;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.debug("Failed to fetch remote document:\n{error}", { error });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (remoteDoc == null) return null;
|
|
275
|
+
const snapshot = options.verifyPortableObject == null ? null : createSnapshotContextLoader(options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }));
|
|
276
|
+
try {
|
|
277
|
+
let object;
|
|
278
|
+
let documentUrl;
|
|
279
|
+
try {
|
|
280
|
+
documentUrl = parseIri(remoteDoc.documentUrl);
|
|
281
|
+
object = await Object$1.fromJsonLd(remoteDoc.document, {
|
|
282
|
+
documentLoader,
|
|
283
|
+
contextLoader: snapshot?.loader ?? options.contextLoader,
|
|
284
|
+
tracerProvider: options.tracerProvider,
|
|
285
|
+
verifyPortableObject: options.verifyPortableObject,
|
|
286
|
+
baseUrl: documentUrl
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof TypeError) {
|
|
290
|
+
logger.debug("Failed to parse JSON-LD document: {error}\n{document}", {
|
|
291
|
+
...remoteDoc,
|
|
292
|
+
error
|
|
293
|
+
});
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
if (snapshot != null) {
|
|
299
|
+
const claim = getPortableResponseClaim(remoteDoc.documentUrl, object.id);
|
|
300
|
+
if (claim === null) {
|
|
301
|
+
logger.debug("Refusing the document {documentUrl}, as it claims to be a portable object with a malformed compatible identifier.", { documentUrl: remoteDoc.documentUrl });
|
|
302
|
+
return null;
|
|
303
|
+
} else if (claim != null) {
|
|
304
|
+
const explicit = getExplicitGateways(portable, claim.id);
|
|
305
|
+
return await dereference(portable, claim.id, claim.inferredGateways, explicit, {
|
|
306
|
+
response: remoteDoc,
|
|
307
|
+
contextLoader: snapshot.loader
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (object.id != null && (options.crossOrigin !== "trust" || isPortableIri(object.id)) && !haveSameIriOrigin(object.id, documentUrl) && !haveSameFe34Origin(object.id, documentUrl)) {
|
|
312
|
+
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".`);
|
|
313
|
+
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\".", {
|
|
314
|
+
...remoteDoc,
|
|
315
|
+
objectId: object.id.href
|
|
316
|
+
});
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return object;
|
|
320
|
+
} finally {
|
|
321
|
+
snapshot?.release();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const PORTABLE_IRI_PATTERN = /^ap(?:\+ef61)?:/i;
|
|
325
|
+
function getExplicitGateways(lookup, id) {
|
|
326
|
+
if (lookup.options.gateways == null) return void 0;
|
|
327
|
+
return lookup.explicitGateways ??= getPortableGatewayCandidates(id, lookup.options.gateways);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Parses a raw portable IRI. URL parsing normalizes dot segments in the
|
|
331
|
+
* opaque path, which would make the parsed IRI identify another portable
|
|
332
|
+
* object, so such IRIs are refused.
|
|
333
|
+
*/
|
|
334
|
+
function parsePortableCandidate(iri) {
|
|
335
|
+
try {
|
|
336
|
+
const parsed = parseIri(iri);
|
|
337
|
+
if (canonicalizePortableUri(iri) !== canonicalizePortableUri(formatIri(parsed))) {
|
|
338
|
+
logger.debug("Refusing to look up the portable IRI {iri}, as its path cannot be represented without changing the identified object.", { iri });
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return parsed;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error instanceof TypeError) {
|
|
344
|
+
logger.debug("Invalid portable IRI {iri}: {error}", {
|
|
345
|
+
iri,
|
|
346
|
+
error
|
|
347
|
+
});
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Recognizes an FEP-ef61 compatible identifier to look up as a portable
|
|
355
|
+
* object.
|
|
356
|
+
* @returns `undefined` if the URL should be fetched as an ordinary HTTP(S)
|
|
357
|
+
* URL, i.e., it is not a compatible identifier, or the
|
|
358
|
+
* `verifyPortableObject` option is not given; `null` if it is
|
|
359
|
+
* a malformed compatible identifier; otherwise, the portable ID and
|
|
360
|
+
* the gateway to ask.
|
|
361
|
+
*/
|
|
362
|
+
function getCompatibleCandidate(href, options) {
|
|
363
|
+
if (options.verifyPortableObject == null) return void 0;
|
|
364
|
+
return parseCompatibleEf61Reference(href);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Gets the origin of the WebFinger server that {@link lookupWebFinger} asks
|
|
368
|
+
* for the identifier, which is also the first gateway of a portable actor.
|
|
369
|
+
*/
|
|
370
|
+
function getWebFingerGateway(identifier) {
|
|
371
|
+
let url;
|
|
372
|
+
if (identifier.protocol === "acct:") {
|
|
373
|
+
const host = extractHandleHost(identifier.pathname);
|
|
374
|
+
if (host == null) return void 0;
|
|
375
|
+
url = new URL(`https://${host}/`);
|
|
376
|
+
} else if (identifier.protocol === "http:" || identifier.protocol === "https:") url = new URL(identifier.origin);
|
|
377
|
+
else return;
|
|
378
|
+
return url.username === "" && url.password === "" ? url : void 0;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Looks up a portable object through FEP-ef61 gateways.
|
|
382
|
+
* @param inferredGateways Gateways inferred from a compatible identifier or
|
|
383
|
+
* WebFinger. If omitted, location hints in the IRI
|
|
384
|
+
* are used unless explicit gateways were given.
|
|
385
|
+
*/
|
|
386
|
+
async function lookupPortableObject(lookup, id, inferredGateways) {
|
|
387
|
+
const { options } = lookup;
|
|
388
|
+
const iri = formatIri(id);
|
|
389
|
+
const explicitGateways = getExplicitGateways(lookup, id);
|
|
390
|
+
if (options.verifyPortableObject == null) {
|
|
391
|
+
logger.debug("Cannot look up the portable object {iri}, as the verifyPortableObject option is not given.", { iri });
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const canonicalId = canonicalizePortableUri(iri);
|
|
395
|
+
const inferred = inferredGateways ?? (explicitGateways == null ? getPortableGatewayCandidates(id) : []);
|
|
396
|
+
const candidates = [...inferred];
|
|
397
|
+
for (const gateway of explicitGateways ?? []) if (!candidates.some((candidate) => candidate.href === gateway.href)) candidates.push(gateway);
|
|
398
|
+
if (candidates.length < 1 && inferredGateways == null) {
|
|
399
|
+
const key = `${canonicalId} `;
|
|
400
|
+
if (lookup.remaining < 1 || lookup.attempted.has(key)) return null;
|
|
401
|
+
lookup.attempted.add(key);
|
|
402
|
+
lookup.remaining--;
|
|
403
|
+
return await dereference(lookup, id, [], explicitGateways == null ? void 0 : []);
|
|
404
|
+
}
|
|
405
|
+
const selected = [];
|
|
406
|
+
for (const gateway of candidates) {
|
|
407
|
+
if (selected.length >= lookup.remaining) break;
|
|
408
|
+
const key = `${canonicalId} ${gateway.href}`;
|
|
409
|
+
if (lookup.attempted.has(key)) continue;
|
|
410
|
+
lookup.attempted.add(key);
|
|
411
|
+
selected.push(gateway);
|
|
412
|
+
}
|
|
413
|
+
if (selected.length < 1) return null;
|
|
414
|
+
lookup.remaining -= selected.length;
|
|
415
|
+
const selectedInferred = selected.filter((candidate) => inferred.some((gateway) => gateway.href === candidate.href));
|
|
416
|
+
const selectedExplicit = explicitGateways?.filter((gateway) => selected.some((candidate) => candidate.href === gateway.href));
|
|
417
|
+
return await dereference(lookup, id, selectedInferred, selectedExplicit);
|
|
418
|
+
}
|
|
419
|
+
async function dereference({ options, documentLoader }, id, inferredGateways, gateways, extra = {}) {
|
|
420
|
+
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
421
|
+
try {
|
|
422
|
+
return await dereferencePortableIri(id, {
|
|
423
|
+
documentLoader,
|
|
424
|
+
contextLoader: extra.contextLoader ?? options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }),
|
|
425
|
+
tracerProvider,
|
|
426
|
+
inferredGateways,
|
|
427
|
+
...gateways == null ? {} : { gateways },
|
|
428
|
+
response: extra.response,
|
|
429
|
+
verifyPortableObject: options.verifyPortableObject,
|
|
430
|
+
crossOrigin: options.crossOrigin === "throw" ? "throw" : "ignore",
|
|
431
|
+
signal: options.signal,
|
|
432
|
+
parse: (document, { contextLoader, baseUrl }) => Object$1.fromJsonLd(document, {
|
|
433
|
+
documentLoader,
|
|
434
|
+
contextLoader,
|
|
435
|
+
tracerProvider,
|
|
436
|
+
verifyPortableObject: options.verifyPortableObject,
|
|
437
|
+
baseUrl
|
|
438
|
+
})
|
|
439
|
+
});
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error instanceof PortableObjectRejectedError) throw error;
|
|
442
|
+
logger.debug("Failed to look up the portable object {iri}:\n{error}", {
|
|
443
|
+
iri: formatIri(id),
|
|
444
|
+
error
|
|
445
|
+
});
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Traverses a collection, yielding each item in the collection.
|
|
451
|
+
* If the collection is paginated, it will fetch the next page
|
|
452
|
+
* automatically.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ``` typescript
|
|
456
|
+
* const collection = await lookupObject(collectionUrl);
|
|
457
|
+
* if (collection instanceof Collection) {
|
|
458
|
+
* for await (const item of traverseCollection(collection)) {
|
|
459
|
+
* console.log(item.id?.href);
|
|
460
|
+
* }
|
|
461
|
+
* }
|
|
462
|
+
* ```
|
|
463
|
+
*
|
|
464
|
+
* @param collection The collection to traverse.
|
|
465
|
+
* @param options Options for traversing the collection.
|
|
466
|
+
* @returns An async iterable of each item in the collection.
|
|
467
|
+
* @since 1.1.0
|
|
468
|
+
*/
|
|
469
|
+
async function* traverseCollection(collection, options = {}) {
|
|
470
|
+
const interval = Temporal.Duration.from(options.interval ?? { seconds: 0 }).total("millisecond");
|
|
471
|
+
let page = await collection.getFirst(options);
|
|
472
|
+
if (page == null) for await (const item of collection.getItems(options)) yield item;
|
|
473
|
+
else while (page != null) {
|
|
474
|
+
for await (const item of page.getItems(options)) yield item;
|
|
475
|
+
if (interval > 0) await delay(interval);
|
|
476
|
+
page = await page.getNext(options);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
export { traverseCollection as n, lookupObject as t };
|