@fedify/vocab 2.4.0-dev.2030 → 2.4.0-dev.2047
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 +5472 -645
- package/dist/mod.d.cts +416 -1
- package/dist/mod.d.ts +416 -1
- package/dist/mod.js +5472 -645
- package/dist-tests/{actor-DMXlfUoB.mjs → actor-chbmxipX.mjs} +71 -2
- package/dist-tests/actor.test.mjs +3 -3
- package/dist-tests/{esm-C_FjLvhG.mjs → esm-Df0Yo-Tp.mjs} +1 -1
- package/dist-tests/gateway.test.mjs +604 -0
- package/dist-tests/{lookup-DF2wJiTc.mjs → lookup-CvrMdVSJ.mjs} +211 -9
- package/dist-tests/lookup.test.mjs +3 -3
- package/dist-tests/portable-lookup.test.mjs +335 -0
- package/dist-tests/signed-representation.test.mjs +1 -1
- package/dist-tests/translation.test.mjs +3 -3
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/{vocab-Dw_Vm0NE.mjs → vocab-BJGPw--L.mjs} +5109 -551
- package/dist-tests/vocab.test.mjs +9 -4
- package/package.json +5 -5
- package/src/actor.ts +110 -1
- package/src/gateway.test.ts +872 -0
- package/src/lookup.ts +302 -2
- package/src/portable-lookup.test.ts +457 -0
- package/src/vocab.test.ts +8 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
|
-
import { A as Offer, B as QuoteRequest, C as InteractionPolicy, D as Multikey, E as Measure, F as Place, H as Service, I as PropertyValue, L as Proposal, M as OrderedCollectionPage, N as Organization, O as Note, P as Person, R as Question, S as Intent, T as Link, U as Source, V as Reject, W as Tombstone, Y as vocab_exports, _ as FeaturedItem, a as Application, b as Hashtag, c as Commitment, d as Delete, f as Document, g as FeaturedCollection, h as FeatureRequest, i as Announce, k as Object$1, l as Create, m as FeatureAuthorization, n as Activity, p as Endpoints, r as Agreement, s as Collection, t as Accept, u as CryptographicKey, v as Follow, w as InteractionRule, x as Image, y as Group, z as QuoteAuthorization } from "./vocab-
|
|
3
|
+
import { A as Offer, B as QuoteRequest, C as InteractionPolicy, D as Multikey, E as Measure, F as Place, H as Service, I as PropertyValue, L as Proposal, M as OrderedCollectionPage, N as Organization, O as Note, P as Person, R as Question, S as Intent, T as Link, U as Source, V as Reject, W as Tombstone, Y as vocab_exports, _ as FeaturedItem, a as Application, b as Hashtag, c as Commitment, d as Delete, f as Document, g as FeaturedCollection, h as FeatureRequest, i as Announce, k as Object$1, l as Create, m as FeatureAuthorization, n as Activity, p as Endpoints, r as Agreement, s as Collection, t as Accept, u as CryptographicKey, v as Follow, w as InteractionRule, x as Image, y as Group, z as QuoteAuthorization } from "./vocab-BJGPw--L.mjs";
|
|
4
4
|
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
5
5
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
6
6
|
import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
|
|
7
|
-
import { configure, reset } from "@logtape/logtape";
|
|
8
7
|
import { LanguageString, decodeMultibase, parseDecimal } from "@fedify/vocab-runtime";
|
|
8
|
+
import { configure, reset } from "@logtape/logtape";
|
|
9
9
|
import { pascalCase } from "es-toolkit";
|
|
10
10
|
import { areAllScalarTypes, loadSchemaFiles } from "@fedify/vocab-tools";
|
|
11
11
|
//#region src/vocab.test.ts
|
|
@@ -1227,7 +1227,8 @@ test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
|
1227
1227
|
contextLoader: mockDocumentLoader
|
|
1228
1228
|
})).getObject({
|
|
1229
1229
|
documentLoader,
|
|
1230
|
-
contextLoader: mockDocumentLoader
|
|
1230
|
+
contextLoader: mockDocumentLoader,
|
|
1231
|
+
verifyPortableObject: async () => ({ verified: true })
|
|
1231
1232
|
}), Note);
|
|
1232
1233
|
deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
|
|
1233
1234
|
});
|
|
@@ -2870,7 +2871,11 @@ test("FEP-fe34: Portable DID authorities are cross-origin boundaries", async ()
|
|
|
2870
2871
|
};
|
|
2871
2872
|
throw new Error("Document not found");
|
|
2872
2873
|
};
|
|
2873
|
-
const result = await create.getObject({
|
|
2874
|
+
const result = await create.getObject({
|
|
2875
|
+
documentLoader,
|
|
2876
|
+
contextLoader: mockDocumentLoader,
|
|
2877
|
+
verifyPortableObject: async () => ({ verified: true })
|
|
2878
|
+
});
|
|
2874
2879
|
assertInstanceOf(result, Note);
|
|
2875
2880
|
deepStrictEqual(result.content, "Fetched portable note");
|
|
2876
2881
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.2047+140b94db",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"jsonld": "^9.0.0",
|
|
44
44
|
"pkijs": "^3.3.3",
|
|
45
45
|
"temporal-polyfill": "^1.0.1",
|
|
46
|
-
"@fedify/
|
|
47
|
-
"@fedify/vocab-
|
|
48
|
-
"@fedify/
|
|
46
|
+
"@fedify/vocab-tools": "2.4.0-dev.2047+140b94db",
|
|
47
|
+
"@fedify/vocab-runtime": "2.4.0-dev.2047+140b94db",
|
|
48
|
+
"@fedify/webfinger": "2.4.0-dev.2047+140b94db"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
51
|
+
"@types/node": "^24.12.2",
|
|
52
52
|
"fast-check": "^3.22.0",
|
|
53
53
|
"fetch-mock": "^12.5.4",
|
|
54
54
|
"tsdown": "^0.22.0",
|
package/src/actor.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
canonicalizePortableUri,
|
|
4
|
+
formatIri,
|
|
5
|
+
fromCompatibleEf61Id,
|
|
6
|
+
isGatewayUrl,
|
|
7
|
+
} from "@fedify/vocab-runtime";
|
|
8
|
+
import { lookupWebFinger, type ResourceDescriptor } from "@fedify/webfinger";
|
|
3
9
|
import {
|
|
4
10
|
type Attributes,
|
|
5
11
|
type Counter,
|
|
@@ -93,6 +99,11 @@ function getActorDiscoveryRemoteHost(
|
|
|
93
99
|
): string | undefined {
|
|
94
100
|
const id = actor instanceof URL ? actor : actor.id;
|
|
95
101
|
if (id == null) return undefined;
|
|
102
|
+
// The authority of a portable ID is a DID, not a host; the handle of
|
|
103
|
+
// a portable actor comes from its first gateway:
|
|
104
|
+
if (isPortableActorId(id)) {
|
|
105
|
+
return actor instanceof URL ? undefined : actor.gateway?.host;
|
|
106
|
+
}
|
|
96
107
|
return id.host === "" ? undefined : id.host;
|
|
97
108
|
}
|
|
98
109
|
|
|
@@ -234,6 +245,17 @@ export interface GetActorHandleOptions extends NormalizeActorHandleOptions {
|
|
|
234
245
|
* await getActorHandle(new URL("https://fosstodon.org/users/hongminhee"));
|
|
235
246
|
* ```
|
|
236
247
|
*
|
|
248
|
+
* For an [FEP-ef61] portable actor, whose ID is an `ap:` or `ap+ef61:` URI,
|
|
249
|
+
* the domain is taken from the first gateway in the actor's `gateways` rather
|
|
250
|
+
* than from its ID. The handle is returned only if its WebFinger response
|
|
251
|
+
* links back to the actor, since the actor's `gateways` are claimed by the
|
|
252
|
+
* actor itself; a failed WebFinger lookup is not enough information to get
|
|
253
|
+
* the handle. A portable actor without `gateways` or
|
|
254
|
+
* `preferredUsername`, or a portable actor URI (which does not tell its
|
|
255
|
+
* gateways), is not supported.
|
|
256
|
+
*
|
|
257
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
258
|
+
*
|
|
237
259
|
* @param actor The actor or actor URI to get the handle from.
|
|
238
260
|
* @param options The extra options for getting the actor handle.
|
|
239
261
|
* @returns The actor handle. It starts with `@` and is followed by the
|
|
@@ -302,6 +324,10 @@ async function getActorHandleInternal(
|
|
|
302
324
|
options: GetActorHandleOptions = {},
|
|
303
325
|
): Promise<`@${string}@${string}` | `${string}@${string}`> {
|
|
304
326
|
const actorId = actor instanceof URL ? actor : actor.id;
|
|
327
|
+
if (actorId != null && isPortableActorId(actorId)) {
|
|
328
|
+
if (actor instanceof URL) throw new ActorHandleNotFoundError();
|
|
329
|
+
return await getPortableActorHandle(actor, actorId, options);
|
|
330
|
+
}
|
|
305
331
|
if (actorId != null) {
|
|
306
332
|
const result = await lookupWebFinger(actorId, {
|
|
307
333
|
userAgent: options.userAgent,
|
|
@@ -344,6 +370,89 @@ async function getActorHandleInternal(
|
|
|
344
370
|
throw new ActorHandleNotFoundError();
|
|
345
371
|
}
|
|
346
372
|
|
|
373
|
+
function isPortableActorId(id: URL): boolean {
|
|
374
|
+
return id.protocol === "ap:" || id.protocol === "ap+ef61:";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The reverse discovery of an FEP-ef61 portable actor's WebFinger address,
|
|
379
|
+
* which takes the domain from the first gateway in its `gateways`.
|
|
380
|
+
*/
|
|
381
|
+
async function getPortableActorHandle(
|
|
382
|
+
actor: Actor,
|
|
383
|
+
actorId: URL,
|
|
384
|
+
options: GetActorHandleOptions,
|
|
385
|
+
): Promise<`@${string}@${string}` | `${string}@${string}`> {
|
|
386
|
+
const gateway = actor.gateway;
|
|
387
|
+
const username = actor.preferredUsername?.toString();
|
|
388
|
+
if (gateway == null || !isGatewayUrl(gateway) || username == null) {
|
|
389
|
+
throw new ActorHandleNotFoundError();
|
|
390
|
+
}
|
|
391
|
+
const canonicalId = canonicalizePortableUri(formatIri(actorId));
|
|
392
|
+
const handle = `acct:${username}@${gateway.host}`;
|
|
393
|
+
const result = await lookupWebFinger(handle, {
|
|
394
|
+
userAgent: options.userAgent,
|
|
395
|
+
tracerProvider: options.tracerProvider,
|
|
396
|
+
meterProvider: options.meterProvider,
|
|
397
|
+
});
|
|
398
|
+
// Unlike an ordinary actor, whose ID vouches for its host, a portable
|
|
399
|
+
// actor's gateways are claimed by the actor itself, so there is no
|
|
400
|
+
// fallback when the gateway does not link back to the actor:
|
|
401
|
+
if (result == null || !linksToPortableActor(result, canonicalId)) {
|
|
402
|
+
throw new ActorHandleNotFoundError();
|
|
403
|
+
}
|
|
404
|
+
// The subject is the canonical address when it differs from the queried
|
|
405
|
+
// one; follow it once if it also links back to the actor:
|
|
406
|
+
const subject = result.subject?.match(/^acct:([^@]+)@([^@]+)$/);
|
|
407
|
+
if (subject != null && result.subject !== handle) {
|
|
408
|
+
const subjectResult = await lookupWebFinger(result.subject!, {
|
|
409
|
+
userAgent: options.userAgent,
|
|
410
|
+
tracerProvider: options.tracerProvider,
|
|
411
|
+
meterProvider: options.meterProvider,
|
|
412
|
+
});
|
|
413
|
+
if (
|
|
414
|
+
subjectResult != null && linksToPortableActor(subjectResult, canonicalId)
|
|
415
|
+
) {
|
|
416
|
+
return normalizeActorHandle(`@${subject[1]}@${subject[2]}`, options);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return normalizeActorHandle(`@${username}@${gateway.host}`, options);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Checks if the first ActivityStreams `self` link of a WebFinger response
|
|
424
|
+
* identifies the portable actor, either by its portable ID or by
|
|
425
|
+
* a compatible identifier.
|
|
426
|
+
*/
|
|
427
|
+
function linksToPortableActor(
|
|
428
|
+
jrd: ResourceDescriptor,
|
|
429
|
+
canonicalId: string,
|
|
430
|
+
): boolean {
|
|
431
|
+
const link = jrd.links?.find((l) =>
|
|
432
|
+
l.rel === "self" && l.href != null &&
|
|
433
|
+
(l.type === "application/activity+json" ||
|
|
434
|
+
l.type?.match(
|
|
435
|
+
/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/,
|
|
436
|
+
) != null)
|
|
437
|
+
);
|
|
438
|
+
if (link?.href == null) return false;
|
|
439
|
+
try {
|
|
440
|
+
let id: string;
|
|
441
|
+
if (/^ap(?:\+ef61)?:/i.test(link.href)) id = link.href;
|
|
442
|
+
else {
|
|
443
|
+
const portable = URL.canParse(link.href)
|
|
444
|
+
? fromCompatibleEf61Id(link.href)
|
|
445
|
+
: null;
|
|
446
|
+
if (portable == null) return false;
|
|
447
|
+
id = formatIri(portable);
|
|
448
|
+
}
|
|
449
|
+
return canonicalizePortableUri(id) === canonicalId;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error instanceof TypeError) return false;
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
347
456
|
async function verifyCrossOriginActorHandle(
|
|
348
457
|
actorId: string,
|
|
349
458
|
alias: string,
|