@fedify/vocab 2.4.0-pr.934.40 → 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 +31713 -8698
- package/dist/mod.d.cts +4708 -7
- package/dist/mod.d.ts +4708 -7
- package/dist/mod.js +31606 -8598
- package/dist-tests/actor-D7_iX4I4.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/application.yaml +15 -0
- package/dist-tests/audio.yaml +2 -1
- package/dist-tests/document.yaml +14 -1
- package/dist-tests/{actor-CnnyTI7i.mjs → esm-DP2Spt0u.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/group.yaml +15 -0
- package/dist-tests/image.yaml +2 -1
- package/dist-tests/link.yaml +14 -1
- 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/organization.yaml +15 -0
- package/dist-tests/page.yaml +2 -1
- package/dist-tests/person.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/service.yaml +15 -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/video.yaml +2 -1
- package/dist-tests/{vocab-OU5GYbNg.mjs → vocab-COZcxf00.mjs} +31227 -8501
- package/dist-tests/vocab.test.mjs +824 -6
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +345 -30
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/application.yaml +15 -0
- package/src/audio.yaml +2 -1
- package/src/document.yaml +14 -1
- package/src/gateway.test.ts +2637 -0
- package/src/group.yaml +15 -0
- package/src/image.yaml +2 -1
- package/src/link.yaml +14 -1
- 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/organization.yaml +15 -0
- package/src/page.yaml +2 -1
- package/src/person.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +4 -2
- package/src/service.yaml +15 -0
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/video.yaml +2 -1
- package/src/vocab.test.ts +1155 -2
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
package/src/lookup.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
GetUserAgentOptions,
|
|
3
|
+
PortableObjectVerifier,
|
|
4
|
+
} from "@fedify/vocab-runtime";
|
|
2
5
|
import {
|
|
6
|
+
canonicalizePortableUri,
|
|
3
7
|
type DocumentLoader,
|
|
8
|
+
formatIri,
|
|
4
9
|
getDocumentLoader,
|
|
5
10
|
haveSameFe34Origin,
|
|
6
11
|
haveSameIriOrigin,
|
|
7
12
|
parseIri,
|
|
8
13
|
type RemoteDocument,
|
|
9
14
|
} from "@fedify/vocab-runtime";
|
|
15
|
+
import {
|
|
16
|
+
createSnapshotContextLoader,
|
|
17
|
+
dereferencePortableIri,
|
|
18
|
+
getPortableGatewayCandidates,
|
|
19
|
+
getPortableResponseClaim,
|
|
20
|
+
isPortableIri,
|
|
21
|
+
parseCompatibleEf61Reference,
|
|
22
|
+
PortableObjectRejectedError,
|
|
23
|
+
} from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
10
24
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
11
25
|
import { getLogger } from "@logtape/logtape";
|
|
12
26
|
import {
|
|
@@ -59,6 +73,9 @@ function getLookupRemoteHost(identifier: string | URL): string | undefined {
|
|
|
59
73
|
let url: URL | undefined;
|
|
60
74
|
if (identifier instanceof URL) {
|
|
61
75
|
url = identifier;
|
|
76
|
+
} else if (PORTABLE_IRI_PATTERN.test(identifier)) {
|
|
77
|
+
// The authority of a portable IRI is a DID, not a host:
|
|
78
|
+
return undefined;
|
|
62
79
|
} else {
|
|
63
80
|
try {
|
|
64
81
|
url = new URL(identifier);
|
|
@@ -70,6 +87,7 @@ function getLookupRemoteHost(identifier: string | URL): string | undefined {
|
|
|
70
87
|
return extractHandleHost(stripped);
|
|
71
88
|
}
|
|
72
89
|
}
|
|
90
|
+
if (isPortableIri(url)) return undefined;
|
|
73
91
|
if (url.host !== "") return url.host;
|
|
74
92
|
// `acct:` URIs are opaque (no `//host` form), so the URL host is empty.
|
|
75
93
|
// The user and authority live in `url.pathname` as
|
|
@@ -166,8 +184,58 @@ export interface LookupObjectOptions {
|
|
|
166
184
|
* @since 1.8.0
|
|
167
185
|
*/
|
|
168
186
|
signal?: AbortSignal;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The [FEP-ef61] gateways to try when looking up a portable
|
|
190
|
+
* (`ap:`/`ap+ef61:`) object, in order. Each must be an HTTP(S) origin.
|
|
191
|
+
* An explicit list replaces `@gateway` location hints, including when
|
|
192
|
+
* it is empty. The gateway named by a compatible identifier or the
|
|
193
|
+
* WebFinger server is still tried first. At most five gateways are
|
|
194
|
+
* requested per lookup, including those given here.
|
|
195
|
+
*
|
|
196
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
197
|
+
* @since 2.4.0
|
|
198
|
+
*/
|
|
199
|
+
gateways?: readonly (string | URL)[];
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The [FEP-ef61] policy to apply to portable objects, typically
|
|
203
|
+
* `verifyPortableObject()` from `@fedify/fedify`, which
|
|
204
|
+
* `Context.lookupObject()` uses by default.
|
|
205
|
+
*
|
|
206
|
+
* When it is given, portable `ap:`/`ap+ef61:` identifiers and compatible
|
|
207
|
+
* identifiers (e.g., `https://server.example/.well-known/apgateway/did:...`),
|
|
208
|
+
* whether they are looked up directly or found in the `self` links of
|
|
209
|
+
* a WebFinger response, are fetched through FEP-ef61 gateways. A fetched
|
|
210
|
+
* portable object is returned only if its `@id` identifies the requested
|
|
211
|
+
* portable object and this function accepts it. A document fetched from
|
|
212
|
+
* an ordinary HTTP(S) URL is checked the same way if its final URL or its
|
|
213
|
+
* `@id` is a portable or compatible identifier, instead of being trusted
|
|
214
|
+
* because of its origin. Note that `crossOrigin: "trust"` does not skip
|
|
215
|
+
* these checks.
|
|
216
|
+
*
|
|
217
|
+
* Without it, portable identifiers are not looked up, and compatible
|
|
218
|
+
* identifiers are fetched as ordinary HTTP(S) URLs, whose objects with
|
|
219
|
+
* a portable `@id` are refused as cross-origin objects, even with
|
|
220
|
+
* `crossOrigin: "trust"`.
|
|
221
|
+
*
|
|
222
|
+
* The returned object also uses it by default for dereferencing its
|
|
223
|
+
* properties, as if it were passed to its property accessors.
|
|
224
|
+
*
|
|
225
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
226
|
+
* @since 2.4.0
|
|
227
|
+
*/
|
|
228
|
+
verifyPortableObject?: PortableObjectVerifier;
|
|
169
229
|
}
|
|
170
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The maximum number of requests to FEP-ef61 gateways that a single
|
|
233
|
+
* {@link lookupObject} call makes for portable objects. Gateways come from
|
|
234
|
+
* possibly untrusted WebFinger responses and location hints, so they are
|
|
235
|
+
* bounded to keep a single lookup from fanning out to many servers.
|
|
236
|
+
*/
|
|
237
|
+
const MAX_PORTABLE_ATTEMPTS = 5;
|
|
238
|
+
|
|
171
239
|
/**
|
|
172
240
|
* Looks up an ActivityStreams object by its URI (including `acct:` URIs)
|
|
173
241
|
* or a fediverse handle (e.g., `@user@server` or `user@server`).
|
|
@@ -195,6 +263,14 @@ export interface LookupObjectOptions {
|
|
|
195
263
|
* // returning a `Note` object.
|
|
196
264
|
* ```
|
|
197
265
|
*
|
|
266
|
+
* [FEP-ef61] portable objects, including portable actors found through
|
|
267
|
+
* WebFinger, are looked up only if the `verifyPortableObject` option is
|
|
268
|
+
* given; see {@link LookupObjectOptions.verifyPortableObject}. Pass
|
|
269
|
+
* {@link LookupObjectOptions.gateways} to look up a portable ID without
|
|
270
|
+
* `@gateway` location hints.
|
|
271
|
+
*
|
|
272
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
273
|
+
*
|
|
198
274
|
* @param identifier The URI or fediverse handle to look up.
|
|
199
275
|
* @param options Lookup options.
|
|
200
276
|
* @returns The object, or `null` if not found.
|
|
@@ -272,11 +348,47 @@ async function lookupObjectInternal(
|
|
|
272
348
|
): Promise<Object | null> {
|
|
273
349
|
const documentLoader = options.documentLoader ??
|
|
274
350
|
getDocumentLoader({ userAgent: options.userAgent });
|
|
351
|
+
const portable: PortableLookup = {
|
|
352
|
+
options,
|
|
353
|
+
documentLoader,
|
|
354
|
+
attempted: new Set(),
|
|
355
|
+
remaining: MAX_PORTABLE_ATTEMPTS,
|
|
356
|
+
};
|
|
275
357
|
if (typeof identifier === "string") {
|
|
276
|
-
|
|
358
|
+
if (PORTABLE_IRI_PATTERN.test(identifier)) {
|
|
359
|
+
const candidate = parsePortableCandidate(identifier);
|
|
360
|
+
if (candidate == null) return null;
|
|
361
|
+
return await lookupPortableObject(portable, candidate, undefined);
|
|
362
|
+
}
|
|
363
|
+
// Check the raw spelling before WHATWG URL can erase dot segments from a
|
|
364
|
+
// compatible identifier. It cannot be recovered from the parsed URL.
|
|
365
|
+
if (
|
|
366
|
+
options.verifyPortableObject != null &&
|
|
367
|
+
parseCompatibleEf61Reference(identifier) === null
|
|
368
|
+
) return null;
|
|
369
|
+
identifier = toAcctUrl(identifier) ?? parseIri(identifier);
|
|
370
|
+
}
|
|
371
|
+
if (isPortableIri(identifier)) {
|
|
372
|
+
let candidate: URL;
|
|
373
|
+
try {
|
|
374
|
+
candidate = parseIri(identifier);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof TypeError) return null;
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
return await lookupPortableObject(portable, candidate, undefined);
|
|
277
380
|
}
|
|
278
381
|
let remoteDoc: RemoteDocument | null = null;
|
|
279
382
|
if (identifier.protocol === "http:" || identifier.protocol === "https:") {
|
|
383
|
+
const compatible = getCompatibleCandidate(identifier.href, options);
|
|
384
|
+
if (compatible !== undefined) {
|
|
385
|
+
if (compatible == null) return null;
|
|
386
|
+
return await lookupPortableObject(
|
|
387
|
+
portable,
|
|
388
|
+
compatible.id,
|
|
389
|
+
[compatible.gateway],
|
|
390
|
+
);
|
|
391
|
+
}
|
|
280
392
|
try {
|
|
281
393
|
remoteDoc = await documentLoader(identifier.href, {
|
|
282
394
|
signal: options.signal,
|
|
@@ -295,6 +407,7 @@ async function lookupObjectInternal(
|
|
|
295
407
|
signal: options.signal,
|
|
296
408
|
});
|
|
297
409
|
if (jrd?.links == null) return null;
|
|
410
|
+
const webFingerGateway = getWebFingerGateway(identifier);
|
|
298
411
|
for (const l of jrd.links) {
|
|
299
412
|
if (
|
|
300
413
|
l.type !== "application/activity+json" &&
|
|
@@ -302,6 +415,44 @@ async function lookupObjectInternal(
|
|
|
302
415
|
/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/,
|
|
303
416
|
) || l.rel !== "self" || l.href == null
|
|
304
417
|
) continue;
|
|
418
|
+
if (PORTABLE_IRI_PATTERN.test(l.href)) {
|
|
419
|
+
// FEP-ef61 says the WebFinger host is the actor's first gateway, so
|
|
420
|
+
// ask it first, and then the location hints in the link:
|
|
421
|
+
if (options.verifyPortableObject == null) {
|
|
422
|
+
logger.debug(
|
|
423
|
+
"Skipping the portable self link {href}, as the " +
|
|
424
|
+
"verifyPortableObject option is not given.",
|
|
425
|
+
{ href: l.href },
|
|
426
|
+
);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const candidate = parsePortableCandidate(l.href);
|
|
430
|
+
if (candidate == null) continue;
|
|
431
|
+
const gateways = webFingerGateway == null ? [] : [webFingerGateway];
|
|
432
|
+
if (options.gateways == null) {
|
|
433
|
+
gateways.push(...getPortableGatewayCandidates(candidate));
|
|
434
|
+
}
|
|
435
|
+
const object = await lookupPortableObject(
|
|
436
|
+
portable,
|
|
437
|
+
candidate,
|
|
438
|
+
gateways,
|
|
439
|
+
);
|
|
440
|
+
if (object != null) return object;
|
|
441
|
+
if (options.signal?.aborted) return null;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const compatible = getCompatibleCandidate(l.href, options);
|
|
445
|
+
if (compatible !== undefined) {
|
|
446
|
+
if (compatible == null) continue;
|
|
447
|
+
const object = await lookupPortableObject(
|
|
448
|
+
portable,
|
|
449
|
+
compatible.id,
|
|
450
|
+
[compatible.gateway],
|
|
451
|
+
);
|
|
452
|
+
if (object != null) return object;
|
|
453
|
+
if (options.signal?.aborted) return null;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
305
456
|
try {
|
|
306
457
|
remoteDoc = await documentLoader(l.href, {
|
|
307
458
|
signal: options.signal,
|
|
@@ -314,49 +465,286 @@ async function lookupObjectInternal(
|
|
|
314
465
|
}
|
|
315
466
|
}
|
|
316
467
|
if (remoteDoc == null) return null;
|
|
317
|
-
|
|
318
|
-
|
|
468
|
+
// With verifyPortableObject, the document may turn out to be a portable
|
|
469
|
+
// object, which has to be verified with the same context documents:
|
|
470
|
+
const snapshot = options.verifyPortableObject == null
|
|
471
|
+
? null
|
|
472
|
+
: createSnapshotContextLoader(
|
|
473
|
+
options.contextLoader ??
|
|
474
|
+
getDocumentLoader({ userAgent: options.userAgent }),
|
|
475
|
+
);
|
|
319
476
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
477
|
+
let object: Object;
|
|
478
|
+
let documentUrl: URL;
|
|
479
|
+
try {
|
|
480
|
+
documentUrl = parseIri(remoteDoc.documentUrl);
|
|
481
|
+
object = await Object.fromJsonLd(remoteDoc.document, {
|
|
482
|
+
documentLoader,
|
|
483
|
+
contextLoader: snapshot?.loader ?? options.contextLoader,
|
|
484
|
+
tracerProvider: options.tracerProvider,
|
|
485
|
+
verifyPortableObject: options.verifyPortableObject,
|
|
486
|
+
baseUrl: documentUrl,
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error instanceof TypeError) {
|
|
490
|
+
logger.debug(
|
|
491
|
+
"Failed to parse JSON-LD document: {error}\n{document}",
|
|
492
|
+
{ ...remoteDoc, error },
|
|
493
|
+
);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
if (snapshot != null) {
|
|
499
|
+
// A document whose final URL or @id stands for a portable object is
|
|
500
|
+
// verified as one instead of being trusted because of its origin:
|
|
501
|
+
const claim = getPortableResponseClaim(remoteDoc.documentUrl, object.id);
|
|
502
|
+
if (claim === null) {
|
|
503
|
+
logger.debug(
|
|
504
|
+
"Refusing the document {documentUrl}, as it claims to be " +
|
|
505
|
+
"a portable object with a malformed compatible identifier.",
|
|
506
|
+
{ documentUrl: remoteDoc.documentUrl },
|
|
507
|
+
);
|
|
508
|
+
return null;
|
|
509
|
+
} else if (claim != null) {
|
|
510
|
+
const explicit = getExplicitGateways(portable, claim.id);
|
|
511
|
+
return await dereference(
|
|
512
|
+
portable,
|
|
513
|
+
claim.id,
|
|
514
|
+
claim.inferredGateways,
|
|
515
|
+
explicit,
|
|
516
|
+
{ response: remoteDoc, contextLoader: snapshot.loader },
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
object.id != null &&
|
|
522
|
+
// A portable object belongs to its DID, not to the server that serves
|
|
523
|
+
// it, so crossOrigin: "trust" does not let a server vouch for it:
|
|
524
|
+
(options.crossOrigin !== "trust" || isPortableIri(object.id)) &&
|
|
525
|
+
!haveSameIriOrigin(object.id, documentUrl) &&
|
|
526
|
+
!haveSameFe34Origin(object.id, documentUrl)
|
|
527
|
+
) {
|
|
528
|
+
if (options.crossOrigin === "throw") {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`The object's @id (${object.id.href}) has a different origin than ` +
|
|
531
|
+
`the document URL (${remoteDoc.documentUrl}); refusing to return ` +
|
|
532
|
+
`the object. If you want to bypass this check and are aware of ` +
|
|
533
|
+
`the security implications, set the crossOrigin option to "trust".`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
logger.warn(
|
|
537
|
+
"The object's @id ({objectId}) has a different origin than the " +
|
|
538
|
+
"document URL ({documentUrl}); refusing to return the object. If " +
|
|
539
|
+
"you want to bypass this check and are aware of the security " +
|
|
540
|
+
'implications, set the crossOrigin option to "trust".',
|
|
541
|
+
{ ...remoteDoc, objectId: object.id.href },
|
|
542
|
+
);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
return object;
|
|
546
|
+
} finally {
|
|
547
|
+
snapshot?.release();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const PORTABLE_IRI_PATTERN = /^ap(?:\+ef61)?:/i;
|
|
552
|
+
|
|
553
|
+
interface PortableLookup {
|
|
554
|
+
readonly options: LookupObjectOptions;
|
|
555
|
+
readonly documentLoader: DocumentLoader;
|
|
556
|
+
/** Pairs of a canonical portable ID and a gateway already asked. */
|
|
557
|
+
readonly attempted: Set<string>;
|
|
558
|
+
/** The number of gateway requests left for this lookup. */
|
|
559
|
+
remaining: number;
|
|
560
|
+
/** Validated only when the lookup reaches a portable object. */
|
|
561
|
+
explicitGateways?: URL[];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getExplicitGateways(
|
|
565
|
+
lookup: PortableLookup,
|
|
566
|
+
id: URL,
|
|
567
|
+
): URL[] | undefined {
|
|
568
|
+
if (lookup.options.gateways == null) return undefined;
|
|
569
|
+
return lookup.explicitGateways ??= getPortableGatewayCandidates(
|
|
570
|
+
id,
|
|
571
|
+
lookup.options.gateways,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Parses a raw portable IRI. URL parsing normalizes dot segments in the
|
|
577
|
+
* opaque path, which would make the parsed IRI identify another portable
|
|
578
|
+
* object, so such IRIs are refused.
|
|
579
|
+
*/
|
|
580
|
+
function parsePortableCandidate(iri: string): URL | null {
|
|
581
|
+
try {
|
|
582
|
+
const parsed = parseIri(iri);
|
|
583
|
+
if (
|
|
584
|
+
canonicalizePortableUri(iri) !==
|
|
585
|
+
canonicalizePortableUri(formatIri(parsed))
|
|
586
|
+
) {
|
|
329
587
|
logger.debug(
|
|
330
|
-
"
|
|
331
|
-
|
|
588
|
+
"Refusing to look up the portable IRI {iri}, as its path cannot be " +
|
|
589
|
+
"represented without changing the identified object.",
|
|
590
|
+
{ iri },
|
|
332
591
|
);
|
|
333
592
|
return null;
|
|
334
593
|
}
|
|
594
|
+
return parsed;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (error instanceof TypeError) {
|
|
597
|
+
logger.debug("Invalid portable IRI {iri}: {error}", { iri, error });
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
335
600
|
throw error;
|
|
336
601
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Recognizes an FEP-ef61 compatible identifier to look up as a portable
|
|
606
|
+
* object.
|
|
607
|
+
* @returns `undefined` if the URL should be fetched as an ordinary HTTP(S)
|
|
608
|
+
* URL, i.e., it is not a compatible identifier, or the
|
|
609
|
+
* `verifyPortableObject` option is not given; `null` if it is
|
|
610
|
+
* a malformed compatible identifier; otherwise, the portable ID and
|
|
611
|
+
* the gateway to ask.
|
|
612
|
+
*/
|
|
613
|
+
function getCompatibleCandidate(
|
|
614
|
+
href: string,
|
|
615
|
+
options: LookupObjectOptions,
|
|
616
|
+
): { readonly id: URL; readonly gateway: URL } | null | undefined {
|
|
617
|
+
if (options.verifyPortableObject == null) return undefined;
|
|
618
|
+
return parseCompatibleEf61Reference(href);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Gets the origin of the WebFinger server that {@link lookupWebFinger} asks
|
|
623
|
+
* for the identifier, which is also the first gateway of a portable actor.
|
|
624
|
+
*/
|
|
625
|
+
function getWebFingerGateway(identifier: URL): URL | undefined {
|
|
626
|
+
let url: URL;
|
|
627
|
+
if (identifier.protocol === "acct:") {
|
|
628
|
+
const host = extractHandleHost(identifier.pathname);
|
|
629
|
+
if (host == null) return undefined;
|
|
630
|
+
url = new URL(`https://${host}/`);
|
|
631
|
+
} else if (
|
|
632
|
+
identifier.protocol === "http:" || identifier.protocol === "https:"
|
|
341
633
|
) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
634
|
+
url = new URL(identifier.origin);
|
|
635
|
+
} else {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
return url.username === "" && url.password === "" ? url : undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Looks up a portable object through FEP-ef61 gateways.
|
|
643
|
+
* @param inferredGateways Gateways inferred from a compatible identifier or
|
|
644
|
+
* WebFinger. If omitted, location hints in the IRI
|
|
645
|
+
* are used unless explicit gateways were given.
|
|
646
|
+
*/
|
|
647
|
+
async function lookupPortableObject(
|
|
648
|
+
lookup: PortableLookup,
|
|
649
|
+
id: URL,
|
|
650
|
+
inferredGateways: readonly URL[] | undefined,
|
|
651
|
+
): Promise<Object | null> {
|
|
652
|
+
const { options } = lookup;
|
|
653
|
+
const iri = formatIri(id);
|
|
654
|
+
const explicitGateways = getExplicitGateways(lookup, id);
|
|
655
|
+
if (options.verifyPortableObject == null) {
|
|
656
|
+
logger.debug(
|
|
657
|
+
"Cannot look up the portable object {iri}, as the " +
|
|
658
|
+
"verifyPortableObject option is not given.",
|
|
659
|
+
{ iri },
|
|
660
|
+
);
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const canonicalId = canonicalizePortableUri(iri);
|
|
664
|
+
const inferred = inferredGateways ??
|
|
665
|
+
(explicitGateways == null ? getPortableGatewayCandidates(id) : []);
|
|
666
|
+
const candidates = [...inferred];
|
|
667
|
+
for (const gateway of explicitGateways ?? []) {
|
|
668
|
+
if (!candidates.some((candidate) => candidate.href === gateway.href)) {
|
|
669
|
+
candidates.push(gateway);
|
|
349
670
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
671
|
+
}
|
|
672
|
+
if (candidates.length < 1 && inferredGateways == null) {
|
|
673
|
+
// No gateway to ask; a custom document loader may know how to
|
|
674
|
+
// retrieve the portable IRI itself:
|
|
675
|
+
const key = `${canonicalId} `;
|
|
676
|
+
if (lookup.remaining < 1 || lookup.attempted.has(key)) return null;
|
|
677
|
+
lookup.attempted.add(key);
|
|
678
|
+
lookup.remaining--;
|
|
679
|
+
return await dereference(
|
|
680
|
+
lookup,
|
|
681
|
+
id,
|
|
682
|
+
[],
|
|
683
|
+
explicitGateways == null ? undefined : [],
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const selected: URL[] = [];
|
|
687
|
+
for (const gateway of candidates) {
|
|
688
|
+
if (selected.length >= lookup.remaining) break;
|
|
689
|
+
const key = `${canonicalId} ${gateway.href}`;
|
|
690
|
+
if (lookup.attempted.has(key)) continue;
|
|
691
|
+
lookup.attempted.add(key);
|
|
692
|
+
selected.push(gateway);
|
|
693
|
+
}
|
|
694
|
+
if (selected.length < 1) return null;
|
|
695
|
+
lookup.remaining -= selected.length;
|
|
696
|
+
const selectedInferred = selected.filter((candidate) =>
|
|
697
|
+
inferred.some((gateway) => gateway.href === candidate.href)
|
|
698
|
+
);
|
|
699
|
+
const selectedExplicit = explicitGateways?.filter((gateway) =>
|
|
700
|
+
selected.some((candidate) => candidate.href === gateway.href)
|
|
701
|
+
);
|
|
702
|
+
return await dereference(
|
|
703
|
+
lookup,
|
|
704
|
+
id,
|
|
705
|
+
selectedInferred,
|
|
706
|
+
selectedExplicit,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function dereference(
|
|
711
|
+
{ options, documentLoader }: PortableLookup,
|
|
712
|
+
id: URL,
|
|
713
|
+
inferredGateways: readonly URL[],
|
|
714
|
+
gateways?: readonly URL[],
|
|
715
|
+
extra: { response?: RemoteDocument; contextLoader?: DocumentLoader } = {},
|
|
716
|
+
): Promise<Object | null> {
|
|
717
|
+
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
718
|
+
try {
|
|
719
|
+
return await dereferencePortableIri(id, {
|
|
720
|
+
documentLoader,
|
|
721
|
+
contextLoader: extra.contextLoader ?? options.contextLoader ??
|
|
722
|
+
getDocumentLoader({ userAgent: options.userAgent }),
|
|
723
|
+
tracerProvider,
|
|
724
|
+
// Preserve where the gateways came from for the verifier:
|
|
725
|
+
inferredGateways,
|
|
726
|
+
...(gateways == null ? {} : { gateways }),
|
|
727
|
+
response: extra.response,
|
|
728
|
+
verifyPortableObject: options.verifyPortableObject,
|
|
729
|
+
crossOrigin: options.crossOrigin === "throw" ? "throw" : "ignore",
|
|
730
|
+
signal: options.signal,
|
|
731
|
+
parse: (document, { contextLoader, baseUrl }) =>
|
|
732
|
+
Object.fromJsonLd(document, {
|
|
733
|
+
documentLoader,
|
|
734
|
+
contextLoader,
|
|
735
|
+
tracerProvider,
|
|
736
|
+
verifyPortableObject: options.verifyPortableObject,
|
|
737
|
+
baseUrl,
|
|
738
|
+
}),
|
|
739
|
+
});
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (error instanceof PortableObjectRejectedError) throw error;
|
|
742
|
+
logger.debug(
|
|
743
|
+
"Failed to look up the portable object {iri}:\n{error}",
|
|
744
|
+
{ iri: formatIri(id), error },
|
|
356
745
|
);
|
|
357
746
|
return null;
|
|
358
747
|
}
|
|
359
|
-
return object;
|
|
360
748
|
}
|
|
361
749
|
|
|
362
750
|
/**
|
|
@@ -387,6 +775,45 @@ export interface TraverseCollectionOptions {
|
|
|
387
775
|
* @default `{ seconds: 0 }`
|
|
388
776
|
*/
|
|
389
777
|
interval?: Temporal.Duration | Temporal.DurationLike;
|
|
778
|
+
/**
|
|
779
|
+
* Whether to trust objects whose origin differs from the collection or page
|
|
780
|
+
* that refers to them. See the `crossOrigin` option of property accessors
|
|
781
|
+
* such as `Collection.getItems()`.
|
|
782
|
+
* @since 2.4.0
|
|
783
|
+
*/
|
|
784
|
+
crossOrigin?: "ignore" | "throw" | "trust";
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* The [FEP-ef61] gateways to fetch portable (`ap:`/`ap+ef61:`) pages and
|
|
788
|
+
* items through, in order. See the `gateways` option of property
|
|
789
|
+
* accessors.
|
|
790
|
+
*
|
|
791
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
792
|
+
* @since 2.4.0
|
|
793
|
+
*/
|
|
794
|
+
gateways?: readonly (string | URL)[];
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* The policy to apply to portable pages and items fetched through
|
|
798
|
+
* gateways, typically `verifyPortableObject()` from `@fedify/fedify`.
|
|
799
|
+
* Portable pages and items are not fetched without it. Pages and items
|
|
800
|
+
* that are fetched use it as their default verifier, unless
|
|
801
|
+
* {@link TraverseCollectionOptions.inheritPortableObjectVerifier} is
|
|
802
|
+
* `false`.
|
|
803
|
+
* @since 2.4.0
|
|
804
|
+
*/
|
|
805
|
+
verifyPortableObject?: PortableObjectVerifier;
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Whether pages and items that are newly fetched use
|
|
809
|
+
* {@link TraverseCollectionOptions.verifyPortableObject} as their default
|
|
810
|
+
* verifier for their property accessors. Defaults to `true`. If `false`,
|
|
811
|
+
* they use the default verifier of the object they are obtained from, if
|
|
812
|
+
* any, instead. See the `inheritPortableObjectVerifier` option of property
|
|
813
|
+
* accessors such as `Collection.getItems()`.
|
|
814
|
+
* @since 2.4.0
|
|
815
|
+
*/
|
|
816
|
+
inheritPortableObjectVerifier?: boolean;
|
|
390
817
|
}
|
|
391
818
|
|
|
392
819
|
/**
|
package/src/mod.ts
CHANGED
|
@@ -58,7 +58,16 @@ export * from "./type.ts";
|
|
|
58
58
|
export * from "./vocab.ts";
|
|
59
59
|
export { LanguageString } from "@fedify/vocab-runtime";
|
|
60
60
|
export type {
|
|
61
|
+
Decimal,
|
|
61
62
|
DocumentLoader,
|
|
63
|
+
DocumentLoaderOptions,
|
|
62
64
|
GetUserAgentOptions,
|
|
65
|
+
Json,
|
|
66
|
+
PortableObjectReferrer,
|
|
67
|
+
PortableObjectVerification,
|
|
68
|
+
PortableObjectVerifier,
|
|
69
|
+
PortableObjectVerifierOptions,
|
|
70
|
+
PropertyPreprocessor,
|
|
71
|
+
PropertyPreprocessorContext,
|
|
63
72
|
RemoteDocument,
|
|
64
73
|
} from "@fedify/vocab-runtime";
|
package/src/object.yaml
CHANGED
|
@@ -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"
|
package/src/organization.yaml
CHANGED
|
@@ -7,6 +7,7 @@ entity: true
|
|
|
7
7
|
description: Represents an organization.
|
|
8
8
|
defaultContext:
|
|
9
9
|
- "https://www.w3.org/ns/activitystreams"
|
|
10
|
+
- "https://w3id.org/fep/ef61"
|
|
10
11
|
- "https://w3id.org/security/v1"
|
|
11
12
|
- "https://w3id.org/security/data-integrity/v1"
|
|
12
13
|
- "https://www.w3.org/ns/did/v1"
|
|
@@ -78,6 +79,20 @@ properties:
|
|
|
78
79
|
range:
|
|
79
80
|
- "https://w3id.org/security#Multikey"
|
|
80
81
|
|
|
82
|
+
- pluralName: gateways
|
|
83
|
+
singularName: gateway
|
|
84
|
+
singularAccessor: true
|
|
85
|
+
compactName: gateways
|
|
86
|
+
uri: "https://w3id.org/fep/ef61/gateways"
|
|
87
|
+
container: list
|
|
88
|
+
description: |
|
|
89
|
+
Gateways where the latest version of this portable actor object can be
|
|
90
|
+
retrieved.
|
|
91
|
+
|
|
92
|
+
See [FEP-ef61](https://w3id.org/fep/ef61) for details.
|
|
93
|
+
range:
|
|
94
|
+
- "fedify:gatewayUrl"
|
|
95
|
+
|
|
81
96
|
- singularName: manuallyApprovesFollowers
|
|
82
97
|
functional: true
|
|
83
98
|
compactName: manuallyApprovesFollowers
|
package/src/page.yaml
CHANGED
|
@@ -7,6 +7,7 @@ entity: true
|
|
|
7
7
|
description: Represents a Web Page.
|
|
8
8
|
defaultContext:
|
|
9
9
|
- "https://www.w3.org/ns/activitystreams"
|
|
10
|
-
- "https://w3id.org/security/data-integrity/
|
|
10
|
+
- "https://w3id.org/security/data-integrity/v2"
|
|
11
|
+
- digestMultibase: "https://www.w3.org/ns/credentials/v2#digestMultibase"
|
|
11
12
|
- "https://gotosocial.org/ns"
|
|
12
13
|
properties: []
|
package/src/person.yaml
CHANGED
|
@@ -7,6 +7,7 @@ entity: true
|
|
|
7
7
|
description: Represents an individual person.
|
|
8
8
|
defaultContext:
|
|
9
9
|
- "https://www.w3.org/ns/activitystreams"
|
|
10
|
+
- "https://w3id.org/fep/ef61"
|
|
10
11
|
- "https://w3id.org/security/v1"
|
|
11
12
|
- "https://w3id.org/security/data-integrity/v1"
|
|
12
13
|
- "https://www.w3.org/ns/did/v1"
|
|
@@ -78,6 +79,20 @@ properties:
|
|
|
78
79
|
range:
|
|
79
80
|
- "https://w3id.org/security#Multikey"
|
|
80
81
|
|
|
82
|
+
- pluralName: gateways
|
|
83
|
+
singularName: gateway
|
|
84
|
+
singularAccessor: true
|
|
85
|
+
compactName: gateways
|
|
86
|
+
uri: "https://w3id.org/fep/ef61/gateways"
|
|
87
|
+
container: list
|
|
88
|
+
description: |
|
|
89
|
+
Gateways where the latest version of this portable actor object can be
|
|
90
|
+
retrieved.
|
|
91
|
+
|
|
92
|
+
See [FEP-ef61](https://w3id.org/fep/ef61) for details.
|
|
93
|
+
range:
|
|
94
|
+
- "fedify:gatewayUrl"
|
|
95
|
+
|
|
81
96
|
- singularName: manuallyApprovesFollowers
|
|
82
97
|
functional: true
|
|
83
98
|
compactName: manuallyApprovesFollowers
|