@fedify/vocab 2.3.0-dev.1119 → 2.3.0-dev.1131
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 +43 -1
- package/dist/mod.d.cts +22 -2
- package/dist/mod.d.ts +22 -2
- package/dist/mod.js +43 -1
- package/dist-tests/{deno-C05d783V.mjs → actor-DBM2eKvJ.mjs} +139 -3
- package/dist-tests/actor.test.mjs +1 -137
- package/dist-tests/lookup.test.mjs +149 -2
- package/package.json +4 -4
- package/src/lookup.test.ts +172 -0
- package/src/lookup.ts +108 -1
package/deno.json
CHANGED
package/dist/mod.cjs
CHANGED
|
@@ -33,7 +33,7 @@ let _fedify_vocab_runtime_temporal = require("@fedify/vocab-runtime/temporal");
|
|
|
33
33
|
let es_toolkit = require("es-toolkit");
|
|
34
34
|
//#region deno.json
|
|
35
35
|
var name = "@fedify/vocab";
|
|
36
|
-
var version = "2.3.0-dev.
|
|
36
|
+
var version = "2.3.0-dev.1131+553b59b8";
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region src/type.ts
|
|
39
39
|
function getTypeId(object) {
|
|
@@ -47552,6 +47552,40 @@ function toAcctUrl(handle) {
|
|
|
47552
47552
|
}
|
|
47553
47553
|
//#endregion
|
|
47554
47554
|
//#region src/lookup.ts
|
|
47555
|
+
const objectLookupCounters = /* @__PURE__ */ new WeakMap();
|
|
47556
|
+
function getObjectLookupCounter(meterProvider) {
|
|
47557
|
+
let counter = objectLookupCounters.get(meterProvider);
|
|
47558
|
+
if (counter == null) {
|
|
47559
|
+
counter = meterProvider.getMeter(name, version).createCounter("activitypub.object.lookup", {
|
|
47560
|
+
description: "ActivityStreams object lookups via lookupObject(), classified by whether the resolved value is an Actor.",
|
|
47561
|
+
unit: "{lookup}"
|
|
47562
|
+
});
|
|
47563
|
+
objectLookupCounters.set(meterProvider, counter);
|
|
47564
|
+
}
|
|
47565
|
+
return counter;
|
|
47566
|
+
}
|
|
47567
|
+
function getLookupRemoteHost(identifier) {
|
|
47568
|
+
let url;
|
|
47569
|
+
if (identifier instanceof URL) url = identifier;
|
|
47570
|
+
else try {
|
|
47571
|
+
url = new URL(identifier);
|
|
47572
|
+
} catch {
|
|
47573
|
+
return extractHandleHost(identifier.startsWith("@") ? identifier.slice(1) : identifier);
|
|
47574
|
+
}
|
|
47575
|
+
if (url.hostname !== "") return url.hostname;
|
|
47576
|
+
if (url.protocol === "acct:") return extractHandleHost(url.pathname);
|
|
47577
|
+
}
|
|
47578
|
+
function extractHandleHost(handle) {
|
|
47579
|
+
const at = handle.lastIndexOf("@");
|
|
47580
|
+
if (at < 0 || at >= handle.length - 1) return void 0;
|
|
47581
|
+
const candidate = handle.slice(at + 1);
|
|
47582
|
+
if (/[/?#\s]/.test(candidate)) return void 0;
|
|
47583
|
+
try {
|
|
47584
|
+
return new URL(`https://${candidate}`).hostname || void 0;
|
|
47585
|
+
} catch {
|
|
47586
|
+
return;
|
|
47587
|
+
}
|
|
47588
|
+
}
|
|
47555
47589
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
47556
47590
|
"fedify",
|
|
47557
47591
|
"vocab",
|
|
@@ -47591,8 +47625,10 @@ const logger = (0, _logtape_logtape.getLogger)([
|
|
|
47591
47625
|
*/
|
|
47592
47626
|
async function lookupObject(identifier, options = {}) {
|
|
47593
47627
|
return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
|
|
47628
|
+
let kind = "other";
|
|
47594
47629
|
try {
|
|
47595
47630
|
const result = await lookupObjectInternal(identifier, options);
|
|
47631
|
+
if (result != null) kind = isActor(result) ? "actor" : "object";
|
|
47596
47632
|
if (result == null) span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
|
|
47597
47633
|
else {
|
|
47598
47634
|
if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
|
|
@@ -47612,6 +47648,12 @@ async function lookupObject(identifier, options = {}) {
|
|
|
47612
47648
|
});
|
|
47613
47649
|
throw error;
|
|
47614
47650
|
} finally {
|
|
47651
|
+
if (options.meterProvider != null) {
|
|
47652
|
+
const attributes = { "activitypub.lookup.kind": kind };
|
|
47653
|
+
const host = getLookupRemoteHost(identifier);
|
|
47654
|
+
if (host != null) attributes["activitypub.remote.host"] = host;
|
|
47655
|
+
getObjectLookupCounter(options.meterProvider).add(1, attributes);
|
|
47656
|
+
}
|
|
47615
47657
|
span.end();
|
|
47616
47658
|
}
|
|
47617
47659
|
});
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
2
|
import { Decimal, DocumentLoader, DocumentLoader as DocumentLoader$1, GetUserAgentOptions, GetUserAgentOptions as GetUserAgentOptions$1, LanguageString, LanguageString as LanguageString$1, RemoteDocument } from "@fedify/vocab-runtime";
|
|
3
|
-
import { Span, TracerProvider } from "@opentelemetry/api";
|
|
3
|
+
import { MeterProvider, Span, TracerProvider } from "@opentelemetry/api";
|
|
4
4
|
|
|
5
5
|
//#region src/vocab.d.ts
|
|
6
6
|
/** Describes an object of any kind. The Object type serves as the base type for
|
|
@@ -18296,6 +18296,18 @@ declare function toAcctUrl(handle: string): URL | null;
|
|
|
18296
18296
|
//#endregion
|
|
18297
18297
|
//#region src/lookup.d.ts
|
|
18298
18298
|
/**
|
|
18299
|
+
* The classification of an ActivityStreams lookup performed by
|
|
18300
|
+
* {@link lookupObject}, recorded as `activitypub.lookup.kind` on the
|
|
18301
|
+
* `activitypub.object.lookup` counter.
|
|
18302
|
+
*
|
|
18303
|
+
* - `actor`: the resolved object is an {@link import("./actor.ts").Actor}.
|
|
18304
|
+
* - `object`: the resolved object is a non-actor ActivityStreams object.
|
|
18305
|
+
* - `other`: the lookup did not resolve to an object (not found, network
|
|
18306
|
+
* failure, parse failure).
|
|
18307
|
+
* @since 2.3.0
|
|
18308
|
+
*/
|
|
18309
|
+
type ObjectLookupKind = "actor" | "object" | "other";
|
|
18310
|
+
/**
|
|
18299
18311
|
* Options for the {@link lookupObject} function.
|
|
18300
18312
|
*
|
|
18301
18313
|
* @since 0.2.0
|
|
@@ -18343,6 +18355,14 @@ interface LookupObjectOptions {
|
|
|
18343
18355
|
*/
|
|
18344
18356
|
tracerProvider?: TracerProvider;
|
|
18345
18357
|
/**
|
|
18358
|
+
* The OpenTelemetry meter provider used to record the
|
|
18359
|
+
* `activitypub.object.lookup` counter. If omitted, the counter is not
|
|
18360
|
+
* emitted at all (the helper is opt-in to avoid touching the global
|
|
18361
|
+
* meter provider for callers that do not use OpenTelemetry).
|
|
18362
|
+
* @since 2.3.0
|
|
18363
|
+
*/
|
|
18364
|
+
meterProvider?: MeterProvider;
|
|
18365
|
+
/**
|
|
18346
18366
|
* AbortSignal for cancelling the request.
|
|
18347
18367
|
* @since 1.8.0
|
|
18348
18368
|
*/
|
|
@@ -18511,4 +18531,4 @@ declare function getTypeId(object: Object$1 | Link | null): URL | null;
|
|
|
18511
18531
|
*/
|
|
18512
18532
|
declare function getTypeId(object: Object$1 | Link | null | undefined): URL | null | undefined;
|
|
18513
18533
|
//#endregion
|
|
18514
|
-
export { $EntityType, Accept, Activity, Actor, ActorTypeName, Add, Announce, AnnounceAuthorization, AnnounceRequest, Application, Arrive, Article, Audio, Block, ChatMessage, Collection, CollectionPage, Create, CryptographicKey, DataIntegrityProof, Delete, DidService, Dislike, Document, type DocumentLoader, Emoji, EmojiReact, Endpoints, Event, Export, FediverseHandle, Flag, Follow, GetActorHandleOptions, type GetUserAgentOptions, Group, Hashtag, Ignore, Image, Intent, InteractionPolicy, InteractionRule, IntransitiveActivity, Invite, Join, LanguageString, Leave, Like, LikeAuthorization, LikeRequest, Link, Listen, LookupObjectOptions, Measure, Mention, Move, Multikey, NormalizeActorHandleOptions, Note, Object$1 as Object, Offer, OrderedCollection, OrderedCollectionPage, Organization, PUBLIC_COLLECTION, Page, Person, Place, Profile, PropertyValue, Proposal, Question, QuoteAuthorization, QuoteRequest, Read, Recipient, Reject, Relationship, type RemoteDocument, Remove, ReplyAuthorization, ReplyRequest, Service, Source, TentativeAccept, TentativeReject, Tombstone, Travel, TraverseCollectionOptions, Undo, Update, Video, View, getActorClassByTypeName, getActorHandle, getActorTypeName, getEntityTypeById, getTypeId, isActor, isEntityType, isFediverseHandle, lookupObject, normalizeActorHandle, parseFediverseHandle, toAcctUrl, traverseCollection };
|
|
18534
|
+
export { $EntityType, Accept, Activity, Actor, ActorTypeName, Add, Announce, AnnounceAuthorization, AnnounceRequest, Application, Arrive, Article, Audio, Block, ChatMessage, Collection, CollectionPage, Create, CryptographicKey, DataIntegrityProof, Delete, DidService, Dislike, Document, type DocumentLoader, Emoji, EmojiReact, Endpoints, Event, Export, FediverseHandle, Flag, Follow, GetActorHandleOptions, type GetUserAgentOptions, Group, Hashtag, Ignore, Image, Intent, InteractionPolicy, InteractionRule, IntransitiveActivity, Invite, Join, LanguageString, Leave, Like, LikeAuthorization, LikeRequest, Link, Listen, LookupObjectOptions, Measure, Mention, Move, Multikey, NormalizeActorHandleOptions, Note, Object$1 as Object, ObjectLookupKind, Offer, OrderedCollection, OrderedCollectionPage, Organization, PUBLIC_COLLECTION, Page, Person, Place, Profile, PropertyValue, Proposal, Question, QuoteAuthorization, QuoteRequest, Read, Recipient, Reject, Relationship, type RemoteDocument, Remove, ReplyAuthorization, ReplyRequest, Service, Source, TentativeAccept, TentativeReject, Tombstone, Travel, TraverseCollectionOptions, Undo, Update, Video, View, getActorClassByTypeName, getActorHandle, getActorTypeName, getEntityTypeById, getTypeId, isActor, isEntityType, isFediverseHandle, lookupObject, normalizeActorHandle, parseFediverseHandle, toAcctUrl, traverseCollection };
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { Span, TracerProvider } from "@opentelemetry/api";
|
|
2
|
+
import { MeterProvider, Span, TracerProvider } from "@opentelemetry/api";
|
|
3
3
|
import { Decimal, DocumentLoader, DocumentLoader as DocumentLoader$1, GetUserAgentOptions, GetUserAgentOptions as GetUserAgentOptions$1, LanguageString, LanguageString as LanguageString$1, RemoteDocument } from "@fedify/vocab-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/vocab.d.ts
|
|
@@ -18296,6 +18296,18 @@ declare function toAcctUrl(handle: string): URL | null;
|
|
|
18296
18296
|
//#endregion
|
|
18297
18297
|
//#region src/lookup.d.ts
|
|
18298
18298
|
/**
|
|
18299
|
+
* The classification of an ActivityStreams lookup performed by
|
|
18300
|
+
* {@link lookupObject}, recorded as `activitypub.lookup.kind` on the
|
|
18301
|
+
* `activitypub.object.lookup` counter.
|
|
18302
|
+
*
|
|
18303
|
+
* - `actor`: the resolved object is an {@link import("./actor.ts").Actor}.
|
|
18304
|
+
* - `object`: the resolved object is a non-actor ActivityStreams object.
|
|
18305
|
+
* - `other`: the lookup did not resolve to an object (not found, network
|
|
18306
|
+
* failure, parse failure).
|
|
18307
|
+
* @since 2.3.0
|
|
18308
|
+
*/
|
|
18309
|
+
type ObjectLookupKind = "actor" | "object" | "other";
|
|
18310
|
+
/**
|
|
18299
18311
|
* Options for the {@link lookupObject} function.
|
|
18300
18312
|
*
|
|
18301
18313
|
* @since 0.2.0
|
|
@@ -18343,6 +18355,14 @@ interface LookupObjectOptions {
|
|
|
18343
18355
|
*/
|
|
18344
18356
|
tracerProvider?: TracerProvider;
|
|
18345
18357
|
/**
|
|
18358
|
+
* The OpenTelemetry meter provider used to record the
|
|
18359
|
+
* `activitypub.object.lookup` counter. If omitted, the counter is not
|
|
18360
|
+
* emitted at all (the helper is opt-in to avoid touching the global
|
|
18361
|
+
* meter provider for callers that do not use OpenTelemetry).
|
|
18362
|
+
* @since 2.3.0
|
|
18363
|
+
*/
|
|
18364
|
+
meterProvider?: MeterProvider;
|
|
18365
|
+
/**
|
|
18346
18366
|
* AbortSignal for cancelling the request.
|
|
18347
18367
|
* @since 1.8.0
|
|
18348
18368
|
*/
|
|
@@ -18511,4 +18531,4 @@ declare function getTypeId(object: Object$1 | Link | null): URL | null;
|
|
|
18511
18531
|
*/
|
|
18512
18532
|
declare function getTypeId(object: Object$1 | Link | null | undefined): URL | null | undefined;
|
|
18513
18533
|
//#endregion
|
|
18514
|
-
export { $EntityType, Accept, Activity, Actor, ActorTypeName, Add, Announce, AnnounceAuthorization, AnnounceRequest, Application, Arrive, Article, Audio, Block, ChatMessage, Collection, CollectionPage, Create, CryptographicKey, DataIntegrityProof, Delete, DidService, Dislike, Document, type DocumentLoader, Emoji, EmojiReact, Endpoints, Event, Export, FediverseHandle, Flag, Follow, GetActorHandleOptions, type GetUserAgentOptions, Group, Hashtag, Ignore, Image, Intent, InteractionPolicy, InteractionRule, IntransitiveActivity, Invite, Join, LanguageString, Leave, Like, LikeAuthorization, LikeRequest, Link, Listen, LookupObjectOptions, Measure, Mention, Move, Multikey, NormalizeActorHandleOptions, Note, Object$1 as Object, Offer, OrderedCollection, OrderedCollectionPage, Organization, PUBLIC_COLLECTION, Page, Person, Place, Profile, PropertyValue, Proposal, Question, QuoteAuthorization, QuoteRequest, Read, Recipient, Reject, Relationship, type RemoteDocument, Remove, ReplyAuthorization, ReplyRequest, Service, Source, TentativeAccept, TentativeReject, Tombstone, Travel, TraverseCollectionOptions, Undo, Update, Video, View, getActorClassByTypeName, getActorHandle, getActorTypeName, getEntityTypeById, getTypeId, isActor, isEntityType, isFediverseHandle, lookupObject, normalizeActorHandle, parseFediverseHandle, toAcctUrl, traverseCollection };
|
|
18534
|
+
export { $EntityType, Accept, Activity, Actor, ActorTypeName, Add, Announce, AnnounceAuthorization, AnnounceRequest, Application, Arrive, Article, Audio, Block, ChatMessage, Collection, CollectionPage, Create, CryptographicKey, DataIntegrityProof, Delete, DidService, Dislike, Document, type DocumentLoader, Emoji, EmojiReact, Endpoints, Event, Export, FediverseHandle, Flag, Follow, GetActorHandleOptions, type GetUserAgentOptions, Group, Hashtag, Ignore, Image, Intent, InteractionPolicy, InteractionRule, IntransitiveActivity, Invite, Join, LanguageString, Leave, Like, LikeAuthorization, LikeRequest, Link, Listen, LookupObjectOptions, Measure, Mention, Move, Multikey, NormalizeActorHandleOptions, Note, Object$1 as Object, ObjectLookupKind, Offer, OrderedCollection, OrderedCollectionPage, Organization, PUBLIC_COLLECTION, Page, Person, Place, Profile, PropertyValue, Proposal, Question, QuoteAuthorization, QuoteRequest, Read, Recipient, Reject, Relationship, type RemoteDocument, Remove, ReplyAuthorization, ReplyRequest, Service, Source, TentativeAccept, TentativeReject, Tombstone, Travel, TraverseCollectionOptions, Undo, Update, Video, View, getActorClassByTypeName, getActorHandle, getActorTypeName, getEntityTypeById, getTypeId, isActor, isEntityType, isFediverseHandle, lookupObject, normalizeActorHandle, parseFediverseHandle, toAcctUrl, traverseCollection };
|
package/dist/mod.js
CHANGED
|
@@ -9,7 +9,7 @@ import { isTemporalDuration, isTemporalInstant } from "@fedify/vocab-runtime/tem
|
|
|
9
9
|
import { delay } from "es-toolkit";
|
|
10
10
|
//#region deno.json
|
|
11
11
|
var name = "@fedify/vocab";
|
|
12
|
-
var version = "2.3.0-dev.
|
|
12
|
+
var version = "2.3.0-dev.1131+553b59b8";
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/type.ts
|
|
15
15
|
function getTypeId(object) {
|
|
@@ -47528,6 +47528,40 @@ function toAcctUrl(handle) {
|
|
|
47528
47528
|
}
|
|
47529
47529
|
//#endregion
|
|
47530
47530
|
//#region src/lookup.ts
|
|
47531
|
+
const objectLookupCounters = /* @__PURE__ */ new WeakMap();
|
|
47532
|
+
function getObjectLookupCounter(meterProvider) {
|
|
47533
|
+
let counter = objectLookupCounters.get(meterProvider);
|
|
47534
|
+
if (counter == null) {
|
|
47535
|
+
counter = meterProvider.getMeter(name, version).createCounter("activitypub.object.lookup", {
|
|
47536
|
+
description: "ActivityStreams object lookups via lookupObject(), classified by whether the resolved value is an Actor.",
|
|
47537
|
+
unit: "{lookup}"
|
|
47538
|
+
});
|
|
47539
|
+
objectLookupCounters.set(meterProvider, counter);
|
|
47540
|
+
}
|
|
47541
|
+
return counter;
|
|
47542
|
+
}
|
|
47543
|
+
function getLookupRemoteHost(identifier) {
|
|
47544
|
+
let url;
|
|
47545
|
+
if (identifier instanceof URL) url = identifier;
|
|
47546
|
+
else try {
|
|
47547
|
+
url = new URL(identifier);
|
|
47548
|
+
} catch {
|
|
47549
|
+
return extractHandleHost(identifier.startsWith("@") ? identifier.slice(1) : identifier);
|
|
47550
|
+
}
|
|
47551
|
+
if (url.hostname !== "") return url.hostname;
|
|
47552
|
+
if (url.protocol === "acct:") return extractHandleHost(url.pathname);
|
|
47553
|
+
}
|
|
47554
|
+
function extractHandleHost(handle) {
|
|
47555
|
+
const at = handle.lastIndexOf("@");
|
|
47556
|
+
if (at < 0 || at >= handle.length - 1) return void 0;
|
|
47557
|
+
const candidate = handle.slice(at + 1);
|
|
47558
|
+
if (/[/?#\s]/.test(candidate)) return void 0;
|
|
47559
|
+
try {
|
|
47560
|
+
return new URL(`https://${candidate}`).hostname || void 0;
|
|
47561
|
+
} catch {
|
|
47562
|
+
return;
|
|
47563
|
+
}
|
|
47564
|
+
}
|
|
47531
47565
|
const logger = getLogger([
|
|
47532
47566
|
"fedify",
|
|
47533
47567
|
"vocab",
|
|
@@ -47567,8 +47601,10 @@ const logger = getLogger([
|
|
|
47567
47601
|
*/
|
|
47568
47602
|
async function lookupObject(identifier, options = {}) {
|
|
47569
47603
|
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
|
|
47604
|
+
let kind = "other";
|
|
47570
47605
|
try {
|
|
47571
47606
|
const result = await lookupObjectInternal(identifier, options);
|
|
47607
|
+
if (result != null) kind = isActor(result) ? "actor" : "object";
|
|
47572
47608
|
if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
47573
47609
|
else {
|
|
47574
47610
|
if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
|
|
@@ -47588,6 +47624,12 @@ async function lookupObject(identifier, options = {}) {
|
|
|
47588
47624
|
});
|
|
47589
47625
|
throw error;
|
|
47590
47626
|
} finally {
|
|
47627
|
+
if (options.meterProvider != null) {
|
|
47628
|
+
const attributes = { "activitypub.lookup.kind": kind };
|
|
47629
|
+
const host = getLookupRemoteHost(identifier);
|
|
47630
|
+
if (host != null) attributes["activitypub.remote.host"] = host;
|
|
47631
|
+
getObjectLookupCounter(options.meterProvider).add(1, attributes);
|
|
47632
|
+
}
|
|
47591
47633
|
span.end();
|
|
47592
47634
|
}
|
|
47593
47635
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
|
-
import { A as __commonJSMin, M as __toESM } from "./vocab-B0Z-tH4q.mjs";
|
|
3
|
+
import { A as __commonJSMin, M as __toESM, r as Application, u as Group, v as Organization, w as Service, y as Person } from "./vocab-B0Z-tH4q.mjs";
|
|
4
|
+
import { t as getTypeId } from "./type-Cf-vxmre.mjs";
|
|
5
|
+
import { lookupWebFinger } from "@fedify/webfinger";
|
|
6
|
+
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
7
|
+
import { domainToASCII, domainToUnicode } from "node:url";
|
|
4
8
|
//#endregion
|
|
5
9
|
//#region ../../node_modules/.pnpm/regexparam@3.0.0/node_modules/regexparam/dist/index.mjs
|
|
6
10
|
var import_glob_to_regexp = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -1202,6 +1206,138 @@ var esm_default = new class FetchMock {
|
|
|
1202
1206
|
//#endregion
|
|
1203
1207
|
//#region deno.json
|
|
1204
1208
|
var name = "@fedify/vocab";
|
|
1205
|
-
var version = "2.3.0-dev.
|
|
1209
|
+
var version = "2.3.0-dev.1131+553b59b8";
|
|
1206
1210
|
//#endregion
|
|
1207
|
-
|
|
1211
|
+
//#region src/actor.ts
|
|
1212
|
+
/**
|
|
1213
|
+
* Checks if the given object is an {@link Actor}.
|
|
1214
|
+
* @param object The object to check.
|
|
1215
|
+
* @returns `true` if the given object is an {@link Actor}.
|
|
1216
|
+
*/
|
|
1217
|
+
function isActor(object) {
|
|
1218
|
+
return object instanceof Application || object instanceof Group || object instanceof Organization || object instanceof Person || object instanceof Service;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Gets the type name of the given actor.
|
|
1222
|
+
* @param actor The actor to get the type name of.
|
|
1223
|
+
* @returns The type name of the given actor.
|
|
1224
|
+
*/
|
|
1225
|
+
function getActorTypeName(actor) {
|
|
1226
|
+
if (actor instanceof Application) return "Application";
|
|
1227
|
+
else if (actor instanceof Group) return "Group";
|
|
1228
|
+
else if (actor instanceof Organization) return "Organization";
|
|
1229
|
+
else if (actor instanceof Person) return "Person";
|
|
1230
|
+
else if (actor instanceof Service) return "Service";
|
|
1231
|
+
throw new Error("Unknown actor type.");
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Gets the actor class by the given type name.
|
|
1235
|
+
* @param typeName The type name to get the actor class by.
|
|
1236
|
+
* @returns The actor class by the given type name.
|
|
1237
|
+
*/
|
|
1238
|
+
function getActorClassByTypeName(typeName) {
|
|
1239
|
+
switch (typeName) {
|
|
1240
|
+
case "Application": return Application;
|
|
1241
|
+
case "Group": return Group;
|
|
1242
|
+
case "Organization": return Organization;
|
|
1243
|
+
case "Person": return Person;
|
|
1244
|
+
case "Service": return Service;
|
|
1245
|
+
}
|
|
1246
|
+
throw new Error("Unknown actor type name.");
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Gets the actor handle, of the form `@username@domain`, from the given actor
|
|
1250
|
+
* or an actor URI.
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* ``` typescript
|
|
1254
|
+
* // Get the handle of an actor object:
|
|
1255
|
+
* await getActorHandle(
|
|
1256
|
+
* new Person({ id: new URL("https://fosstodon.org/users/hongminhee") })
|
|
1257
|
+
* );
|
|
1258
|
+
*
|
|
1259
|
+
* // Get the handle of an actor URI:
|
|
1260
|
+
* await getActorHandle(new URL("https://fosstodon.org/users/hongminhee"));
|
|
1261
|
+
* ```
|
|
1262
|
+
*
|
|
1263
|
+
* @param actor The actor or actor URI to get the handle from.
|
|
1264
|
+
* @param options The extra options for getting the actor handle.
|
|
1265
|
+
* @returns The actor handle. It starts with `@` and is followed by the
|
|
1266
|
+
* username and domain, separated by `@` by default (it can be
|
|
1267
|
+
* customized with the options).
|
|
1268
|
+
* @throws {TypeError} If the actor does not have enough information to get the
|
|
1269
|
+
* handle.
|
|
1270
|
+
* @since 0.4.0
|
|
1271
|
+
*/
|
|
1272
|
+
async function getActorHandle(actor, options = {}) {
|
|
1273
|
+
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.get_actor_handle", async (span) => {
|
|
1274
|
+
if (isActor(actor)) {
|
|
1275
|
+
if (actor.id != null) span.setAttribute("activitypub.actor.id", actor.id.href);
|
|
1276
|
+
span.setAttribute("activitypub.actor.type", getTypeId(actor).href);
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
return await getActorHandleInternal(actor, options);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
span.setStatus({
|
|
1282
|
+
code: SpanStatusCode.ERROR,
|
|
1283
|
+
message: String(error)
|
|
1284
|
+
});
|
|
1285
|
+
throw error;
|
|
1286
|
+
} finally {
|
|
1287
|
+
span.end();
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async function getActorHandleInternal(actor, options = {}) {
|
|
1292
|
+
const actorId = actor instanceof URL ? actor : actor.id;
|
|
1293
|
+
if (actorId != null) {
|
|
1294
|
+
const result = await lookupWebFinger(actorId, {
|
|
1295
|
+
userAgent: options.userAgent,
|
|
1296
|
+
tracerProvider: options.tracerProvider
|
|
1297
|
+
});
|
|
1298
|
+
if (result != null) {
|
|
1299
|
+
const aliases = [...result.aliases ?? []];
|
|
1300
|
+
if (result.subject != null) aliases.unshift(result.subject);
|
|
1301
|
+
for (const alias of aliases) {
|
|
1302
|
+
const match = alias.match(/^acct:([^@]+)@([^@]+)$/);
|
|
1303
|
+
if (match != null) {
|
|
1304
|
+
if (new URL(`https://${match[2]}/`).hostname !== actorId.hostname && !await verifyCrossOriginActorHandle(actorId.href, alias, options.userAgent, options.tracerProvider)) continue;
|
|
1305
|
+
return normalizeActorHandle(`@${match[1]}@${match[2]}`, options);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!(actor instanceof URL) && actor.preferredUsername != null && actor.id != null) return normalizeActorHandle(`@${actor.preferredUsername}@${actor.id.host}`, options);
|
|
1311
|
+
throw new TypeError("Actor does not have enough information to get the handle.");
|
|
1312
|
+
}
|
|
1313
|
+
async function verifyCrossOriginActorHandle(actorId, alias, userAgent, tracerProvider) {
|
|
1314
|
+
const response = await lookupWebFinger(alias, {
|
|
1315
|
+
userAgent,
|
|
1316
|
+
tracerProvider
|
|
1317
|
+
});
|
|
1318
|
+
if (response == null) return false;
|
|
1319
|
+
for (const alias of response.aliases ?? []) if (new URL(alias).href === actorId) return true;
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Normalizes the given actor handle.
|
|
1324
|
+
* @param handle The full handle of the actor to normalize.
|
|
1325
|
+
* @param options The options for normalizing the actor handle.
|
|
1326
|
+
* @returns The normalized actor handle.
|
|
1327
|
+
* @throws {TypeError} If the actor handle is invalid.
|
|
1328
|
+
* @since 0.9.0
|
|
1329
|
+
*/
|
|
1330
|
+
function normalizeActorHandle(handle, options = {}) {
|
|
1331
|
+
handle = handle.replace(/^@/, "");
|
|
1332
|
+
const atPos = handle.indexOf("@");
|
|
1333
|
+
if (atPos < 1) throw new TypeError("Invalid actor handle.");
|
|
1334
|
+
let domain = handle.substring(atPos + 1);
|
|
1335
|
+
if (domain.length < 1 || domain.includes("@")) throw new TypeError("Invalid actor handle.");
|
|
1336
|
+
domain = domain.toLowerCase();
|
|
1337
|
+
domain = options.punycode ? domainToASCII(domain) : domainToUnicode(domain);
|
|
1338
|
+
domain = domain.toLowerCase();
|
|
1339
|
+
const user = handle.substring(0, atPos);
|
|
1340
|
+
return options.trimLeadingAt ? `${user}@${domain}` : `@${user}@${domain}`;
|
|
1341
|
+
}
|
|
1342
|
+
//#endregion
|
|
1343
|
+
export { normalizeActorHandle as a, esm_default as c, isActor as i, getActorHandle as n, name as o, getActorTypeName as r, version as s, getActorClassByTypeName as t };
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
3
|
import { j as __exportAll, r as Application, u as Group, v as Organization, w as Service, y as Person } from "./vocab-B0Z-tH4q.mjs";
|
|
4
|
-
import { n as
|
|
5
|
-
import { t as getTypeId } from "./type-Cf-vxmre.mjs";
|
|
4
|
+
import { a as normalizeActorHandle, c as esm_default, i as isActor, n as getActorHandle, r as getActorTypeName, t as getActorClassByTypeName } from "./actor-DBM2eKvJ.mjs";
|
|
6
5
|
import { test } from "@fedify/fixture";
|
|
7
6
|
import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
|
|
8
|
-
import { lookupWebFinger } from "@fedify/webfinger";
|
|
9
|
-
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
10
|
-
import { domainToASCII, domainToUnicode } from "node:url";
|
|
11
7
|
//#region ../../node_modules/.pnpm/fast-check@3.23.2/node_modules/fast-check/lib/esm/check/precondition/PreconditionFailure.js
|
|
12
8
|
var PreconditionFailure = class PreconditionFailure extends Error {
|
|
13
9
|
constructor(interruptExecution = false) {
|
|
@@ -5609,138 +5605,6 @@ function anything(constraints) {
|
|
|
5609
5605
|
return anyArbitraryBuilder(toQualifiedObjectConstraints(constraints));
|
|
5610
5606
|
}
|
|
5611
5607
|
//#endregion
|
|
5612
|
-
//#region src/actor.ts
|
|
5613
|
-
/**
|
|
5614
|
-
* Checks if the given object is an {@link Actor}.
|
|
5615
|
-
* @param object The object to check.
|
|
5616
|
-
* @returns `true` if the given object is an {@link Actor}.
|
|
5617
|
-
*/
|
|
5618
|
-
function isActor(object) {
|
|
5619
|
-
return object instanceof Application || object instanceof Group || object instanceof Organization || object instanceof Person || object instanceof Service;
|
|
5620
|
-
}
|
|
5621
|
-
/**
|
|
5622
|
-
* Gets the type name of the given actor.
|
|
5623
|
-
* @param actor The actor to get the type name of.
|
|
5624
|
-
* @returns The type name of the given actor.
|
|
5625
|
-
*/
|
|
5626
|
-
function getActorTypeName(actor) {
|
|
5627
|
-
if (actor instanceof Application) return "Application";
|
|
5628
|
-
else if (actor instanceof Group) return "Group";
|
|
5629
|
-
else if (actor instanceof Organization) return "Organization";
|
|
5630
|
-
else if (actor instanceof Person) return "Person";
|
|
5631
|
-
else if (actor instanceof Service) return "Service";
|
|
5632
|
-
throw new Error("Unknown actor type.");
|
|
5633
|
-
}
|
|
5634
|
-
/**
|
|
5635
|
-
* Gets the actor class by the given type name.
|
|
5636
|
-
* @param typeName The type name to get the actor class by.
|
|
5637
|
-
* @returns The actor class by the given type name.
|
|
5638
|
-
*/
|
|
5639
|
-
function getActorClassByTypeName(typeName) {
|
|
5640
|
-
switch (typeName) {
|
|
5641
|
-
case "Application": return Application;
|
|
5642
|
-
case "Group": return Group;
|
|
5643
|
-
case "Organization": return Organization;
|
|
5644
|
-
case "Person": return Person;
|
|
5645
|
-
case "Service": return Service;
|
|
5646
|
-
}
|
|
5647
|
-
throw new Error("Unknown actor type name.");
|
|
5648
|
-
}
|
|
5649
|
-
/**
|
|
5650
|
-
* Gets the actor handle, of the form `@username@domain`, from the given actor
|
|
5651
|
-
* or an actor URI.
|
|
5652
|
-
*
|
|
5653
|
-
* @example
|
|
5654
|
-
* ``` typescript
|
|
5655
|
-
* // Get the handle of an actor object:
|
|
5656
|
-
* await getActorHandle(
|
|
5657
|
-
* new Person({ id: new URL("https://fosstodon.org/users/hongminhee") })
|
|
5658
|
-
* );
|
|
5659
|
-
*
|
|
5660
|
-
* // Get the handle of an actor URI:
|
|
5661
|
-
* await getActorHandle(new URL("https://fosstodon.org/users/hongminhee"));
|
|
5662
|
-
* ```
|
|
5663
|
-
*
|
|
5664
|
-
* @param actor The actor or actor URI to get the handle from.
|
|
5665
|
-
* @param options The extra options for getting the actor handle.
|
|
5666
|
-
* @returns The actor handle. It starts with `@` and is followed by the
|
|
5667
|
-
* username and domain, separated by `@` by default (it can be
|
|
5668
|
-
* customized with the options).
|
|
5669
|
-
* @throws {TypeError} If the actor does not have enough information to get the
|
|
5670
|
-
* handle.
|
|
5671
|
-
* @since 0.4.0
|
|
5672
|
-
*/
|
|
5673
|
-
async function getActorHandle(actor, options = {}) {
|
|
5674
|
-
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.get_actor_handle", async (span) => {
|
|
5675
|
-
if (isActor(actor)) {
|
|
5676
|
-
if (actor.id != null) span.setAttribute("activitypub.actor.id", actor.id.href);
|
|
5677
|
-
span.setAttribute("activitypub.actor.type", getTypeId(actor).href);
|
|
5678
|
-
}
|
|
5679
|
-
try {
|
|
5680
|
-
return await getActorHandleInternal(actor, options);
|
|
5681
|
-
} catch (error) {
|
|
5682
|
-
span.setStatus({
|
|
5683
|
-
code: SpanStatusCode.ERROR,
|
|
5684
|
-
message: String(error)
|
|
5685
|
-
});
|
|
5686
|
-
throw error;
|
|
5687
|
-
} finally {
|
|
5688
|
-
span.end();
|
|
5689
|
-
}
|
|
5690
|
-
});
|
|
5691
|
-
}
|
|
5692
|
-
async function getActorHandleInternal(actor, options = {}) {
|
|
5693
|
-
const actorId = actor instanceof URL ? actor : actor.id;
|
|
5694
|
-
if (actorId != null) {
|
|
5695
|
-
const result = await lookupWebFinger(actorId, {
|
|
5696
|
-
userAgent: options.userAgent,
|
|
5697
|
-
tracerProvider: options.tracerProvider
|
|
5698
|
-
});
|
|
5699
|
-
if (result != null) {
|
|
5700
|
-
const aliases = [...result.aliases ?? []];
|
|
5701
|
-
if (result.subject != null) aliases.unshift(result.subject);
|
|
5702
|
-
for (const alias of aliases) {
|
|
5703
|
-
const match = alias.match(/^acct:([^@]+)@([^@]+)$/);
|
|
5704
|
-
if (match != null) {
|
|
5705
|
-
if (new URL(`https://${match[2]}/`).hostname !== actorId.hostname && !await verifyCrossOriginActorHandle(actorId.href, alias, options.userAgent, options.tracerProvider)) continue;
|
|
5706
|
-
return normalizeActorHandle(`@${match[1]}@${match[2]}`, options);
|
|
5707
|
-
}
|
|
5708
|
-
}
|
|
5709
|
-
}
|
|
5710
|
-
}
|
|
5711
|
-
if (!(actor instanceof URL) && actor.preferredUsername != null && actor.id != null) return normalizeActorHandle(`@${actor.preferredUsername}@${actor.id.host}`, options);
|
|
5712
|
-
throw new TypeError("Actor does not have enough information to get the handle.");
|
|
5713
|
-
}
|
|
5714
|
-
async function verifyCrossOriginActorHandle(actorId, alias, userAgent, tracerProvider) {
|
|
5715
|
-
const response = await lookupWebFinger(alias, {
|
|
5716
|
-
userAgent,
|
|
5717
|
-
tracerProvider
|
|
5718
|
-
});
|
|
5719
|
-
if (response == null) return false;
|
|
5720
|
-
for (const alias of response.aliases ?? []) if (new URL(alias).href === actorId) return true;
|
|
5721
|
-
return false;
|
|
5722
|
-
}
|
|
5723
|
-
/**
|
|
5724
|
-
* Normalizes the given actor handle.
|
|
5725
|
-
* @param handle The full handle of the actor to normalize.
|
|
5726
|
-
* @param options The options for normalizing the actor handle.
|
|
5727
|
-
* @returns The normalized actor handle.
|
|
5728
|
-
* @throws {TypeError} If the actor handle is invalid.
|
|
5729
|
-
* @since 0.9.0
|
|
5730
|
-
*/
|
|
5731
|
-
function normalizeActorHandle(handle, options = {}) {
|
|
5732
|
-
handle = handle.replace(/^@/, "");
|
|
5733
|
-
const atPos = handle.indexOf("@");
|
|
5734
|
-
if (atPos < 1) throw new TypeError("Invalid actor handle.");
|
|
5735
|
-
let domain = handle.substring(atPos + 1);
|
|
5736
|
-
if (domain.length < 1 || domain.includes("@")) throw new TypeError("Invalid actor handle.");
|
|
5737
|
-
domain = domain.toLowerCase();
|
|
5738
|
-
domain = options.punycode ? domainToASCII(domain) : domainToUnicode(domain);
|
|
5739
|
-
domain = domain.toLowerCase();
|
|
5740
|
-
const user = handle.substring(0, atPos);
|
|
5741
|
-
return options.trimLeadingAt ? `${user}@${domain}` : `@${user}@${domain}`;
|
|
5742
|
-
}
|
|
5743
|
-
//#endregion
|
|
5744
5608
|
//#region src/actor.test.ts
|
|
5745
5609
|
function actorClass() {
|
|
5746
5610
|
return constantFrom(Application, Group, Organization, Person, Service);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
3
|
import { g as Object$1, h as Note, i as Collection, y as Person } from "./vocab-B0Z-tH4q.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { c as esm_default, i as isActor, o as name, s as version } from "./actor-DBM2eKvJ.mjs";
|
|
5
5
|
import { t as getTypeId } from "./type-Cf-vxmre.mjs";
|
|
6
6
|
import { t as assertInstanceOf } from "./utils-CE8Dk5hm.mjs";
|
|
7
|
-
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
7
|
+
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
8
8
|
import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
|
|
9
9
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
10
10
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
@@ -67,6 +67,40 @@ function toAcctUrl(handle) {
|
|
|
67
67
|
}
|
|
68
68
|
//#endregion
|
|
69
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.hostname !== "") return url.hostname;
|
|
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}`).hostname || void 0;
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
const logger = getLogger([
|
|
71
105
|
"fedify",
|
|
72
106
|
"vocab",
|
|
@@ -106,8 +140,10 @@ const logger = getLogger([
|
|
|
106
140
|
*/
|
|
107
141
|
async function lookupObject(identifier, options = {}) {
|
|
108
142
|
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
|
|
143
|
+
let kind = "other";
|
|
109
144
|
try {
|
|
110
145
|
const result = await lookupObjectInternal(identifier, options);
|
|
146
|
+
if (result != null) kind = isActor(result) ? "actor" : "object";
|
|
111
147
|
if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
112
148
|
else {
|
|
113
149
|
if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
|
|
@@ -127,6 +163,12 @@ async function lookupObject(identifier, options = {}) {
|
|
|
127
163
|
});
|
|
128
164
|
throw error;
|
|
129
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
|
+
}
|
|
130
172
|
span.end();
|
|
131
173
|
}
|
|
132
174
|
});
|
|
@@ -668,5 +710,110 @@ test("lookupObject() records OpenTelemetry span events", async () => {
|
|
|
668
710
|
ok(typeof event.attributes["activitypub.object.json"] === "string");
|
|
669
711
|
deepStrictEqual(JSON.parse(event.attributes["activitypub.object.json"]).id, "https://example.com/object");
|
|
670
712
|
});
|
|
713
|
+
test("lookupObject() records activitypub.object.lookup counter", {
|
|
714
|
+
sanitizeResources: false,
|
|
715
|
+
sanitizeOps: false
|
|
716
|
+
}, async (t) => {
|
|
717
|
+
esm_default.spyGlobal();
|
|
718
|
+
try {
|
|
719
|
+
esm_default.removeRoutes();
|
|
720
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger", {
|
|
721
|
+
subject: "acct:johndoe@example.com",
|
|
722
|
+
links: [{
|
|
723
|
+
rel: "self",
|
|
724
|
+
href: "https://example.com/person",
|
|
725
|
+
type: "application/activity+json"
|
|
726
|
+
}]
|
|
727
|
+
});
|
|
728
|
+
await t.step("records kind=actor for an actor result", async () => {
|
|
729
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
730
|
+
assertInstanceOf(await lookupObject("@johndoe@example.com", {
|
|
731
|
+
documentLoader: mockDocumentLoader,
|
|
732
|
+
contextLoader: mockDocumentLoader,
|
|
733
|
+
meterProvider
|
|
734
|
+
}), Person);
|
|
735
|
+
const counters = recorder.getMeasurements("activitypub.object.lookup");
|
|
736
|
+
deepStrictEqual(counters.length, 1);
|
|
737
|
+
deepStrictEqual(counters[0].type, "counter");
|
|
738
|
+
deepStrictEqual(counters[0].value, 1);
|
|
739
|
+
deepStrictEqual(counters[0].attributes["activitypub.lookup.kind"], "actor");
|
|
740
|
+
deepStrictEqual(counters[0].attributes["activitypub.remote.host"], "example.com");
|
|
741
|
+
deepStrictEqual(recorder.getMeasurements("activitypub.document.fetch").length, 0);
|
|
742
|
+
});
|
|
743
|
+
await t.step("records kind=object for a non-actor object", async () => {
|
|
744
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
745
|
+
assertInstanceOf(await lookupObject("https://example.com/object", {
|
|
746
|
+
documentLoader: mockDocumentLoader,
|
|
747
|
+
contextLoader: mockDocumentLoader,
|
|
748
|
+
meterProvider
|
|
749
|
+
}), Object$1);
|
|
750
|
+
const counters = recorder.getMeasurements("activitypub.object.lookup");
|
|
751
|
+
deepStrictEqual(counters.length, 1);
|
|
752
|
+
deepStrictEqual(counters[0].attributes["activitypub.lookup.kind"], "object");
|
|
753
|
+
deepStrictEqual(counters[0].attributes["activitypub.remote.host"], "example.com");
|
|
754
|
+
});
|
|
755
|
+
await t.step("records kind=other on null result", async () => {
|
|
756
|
+
esm_default.removeRoutes();
|
|
757
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger", {
|
|
758
|
+
subject: "acct:janedoe@example.com",
|
|
759
|
+
links: [{
|
|
760
|
+
rel: "self",
|
|
761
|
+
href: "https://example.com/404",
|
|
762
|
+
type: "application/activity+json"
|
|
763
|
+
}]
|
|
764
|
+
});
|
|
765
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
766
|
+
deepStrictEqual(await lookupObject("janedoe@example.com", {
|
|
767
|
+
documentLoader: mockDocumentLoader,
|
|
768
|
+
contextLoader: mockDocumentLoader,
|
|
769
|
+
meterProvider
|
|
770
|
+
}), null);
|
|
771
|
+
const counters = recorder.getMeasurements("activitypub.object.lookup");
|
|
772
|
+
deepStrictEqual(counters.length, 1);
|
|
773
|
+
deepStrictEqual(counters[0].attributes["activitypub.lookup.kind"], "other");
|
|
774
|
+
deepStrictEqual(counters[0].attributes["activitypub.remote.host"], "example.com");
|
|
775
|
+
});
|
|
776
|
+
await t.step("omits counter when no meterProvider is provided", async () => {
|
|
777
|
+
const [_unused, recorder] = createTestMeterProvider();
|
|
778
|
+
await lookupObject("https://example.com/object", {
|
|
779
|
+
documentLoader: mockDocumentLoader,
|
|
780
|
+
contextLoader: mockDocumentLoader
|
|
781
|
+
});
|
|
782
|
+
deepStrictEqual(recorder.getMeasurements("activitypub.object.lookup").length, 0);
|
|
783
|
+
});
|
|
784
|
+
await t.step("extracts host from a URL acct: instance", async () => {
|
|
785
|
+
esm_default.removeRoutes();
|
|
786
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger", {
|
|
787
|
+
subject: "acct:johndoe@example.com",
|
|
788
|
+
links: [{
|
|
789
|
+
rel: "self",
|
|
790
|
+
href: "https://example.com/person",
|
|
791
|
+
type: "application/activity+json"
|
|
792
|
+
}]
|
|
793
|
+
});
|
|
794
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
795
|
+
await lookupObject(new URL("acct:johndoe@example.com"), {
|
|
796
|
+
documentLoader: mockDocumentLoader,
|
|
797
|
+
contextLoader: mockDocumentLoader,
|
|
798
|
+
meterProvider
|
|
799
|
+
});
|
|
800
|
+
deepStrictEqual(recorder.getMeasurement("activitypub.object.lookup")?.attributes["activitypub.remote.host"], "example.com");
|
|
801
|
+
});
|
|
802
|
+
await t.step("omits host when the identifier carries path/query characters", async () => {
|
|
803
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
804
|
+
await lookupObject("johndoe@example.com/leak", {
|
|
805
|
+
documentLoader: mockDocumentLoader,
|
|
806
|
+
contextLoader: mockDocumentLoader,
|
|
807
|
+
meterProvider
|
|
808
|
+
});
|
|
809
|
+
const counter = recorder.getMeasurement("activitypub.object.lookup");
|
|
810
|
+
ok(counter != null);
|
|
811
|
+
deepStrictEqual("activitypub.remote.host" in counter.attributes, false, "high-cardinality handle suffixes must not be recorded as remote.host");
|
|
812
|
+
});
|
|
813
|
+
} finally {
|
|
814
|
+
esm_default.removeRoutes();
|
|
815
|
+
esm_default.hardReset();
|
|
816
|
+
}
|
|
817
|
+
});
|
|
671
818
|
//#endregion
|
|
672
819
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab",
|
|
3
|
-
"version": "2.3.0-dev.
|
|
3
|
+
"version": "2.3.0-dev.1131+553b59b8",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"es-toolkit": "1.46.1",
|
|
47
47
|
"jsonld": "^9.0.0",
|
|
48
48
|
"pkijs": "^3.3.3",
|
|
49
|
-
"@fedify/
|
|
50
|
-
"@fedify/
|
|
51
|
-
"@fedify/vocab-runtime": "2.3.0-dev.
|
|
49
|
+
"@fedify/vocab-tools": "2.3.0-dev.1131+553b59b8",
|
|
50
|
+
"@fedify/webfinger": "2.3.0-dev.1131+553b59b8",
|
|
51
|
+
"@fedify/vocab-runtime": "2.3.0-dev.1131+553b59b8"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^22.17.0",
|
package/src/lookup.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createTestMeterProvider,
|
|
2
3
|
createTestTracerProvider,
|
|
3
4
|
mockDocumentLoader,
|
|
4
5
|
test,
|
|
@@ -693,4 +694,175 @@ test("lookupObject() records OpenTelemetry span events", async () => {
|
|
|
693
694
|
deepStrictEqual(recordedObject.id, "https://example.com/object");
|
|
694
695
|
});
|
|
695
696
|
|
|
697
|
+
test("lookupObject() records activitypub.object.lookup counter", {
|
|
698
|
+
sanitizeResources: false,
|
|
699
|
+
sanitizeOps: false,
|
|
700
|
+
}, async (t) => {
|
|
701
|
+
fetchMock.spyGlobal();
|
|
702
|
+
// Cleanup must run even if a test step throws so the global `fetch`
|
|
703
|
+
// spy does not leak into subsequent tests.
|
|
704
|
+
try {
|
|
705
|
+
fetchMock.removeRoutes();
|
|
706
|
+
fetchMock.get(
|
|
707
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
708
|
+
{
|
|
709
|
+
subject: "acct:johndoe@example.com",
|
|
710
|
+
links: [
|
|
711
|
+
{
|
|
712
|
+
rel: "self",
|
|
713
|
+
href: "https://example.com/person",
|
|
714
|
+
type: "application/activity+json",
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
},
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
await t.step("records kind=actor for an actor result", async () => {
|
|
721
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
722
|
+
const person = await lookupObject("@johndoe@example.com", {
|
|
723
|
+
documentLoader: mockDocumentLoader,
|
|
724
|
+
contextLoader: mockDocumentLoader,
|
|
725
|
+
meterProvider,
|
|
726
|
+
});
|
|
727
|
+
assertInstanceOf(person, Person);
|
|
728
|
+
const counters = recorder.getMeasurements("activitypub.object.lookup");
|
|
729
|
+
deepStrictEqual(counters.length, 1);
|
|
730
|
+
deepStrictEqual(counters[0].type, "counter");
|
|
731
|
+
deepStrictEqual(counters[0].value, 1);
|
|
732
|
+
deepStrictEqual(
|
|
733
|
+
counters[0].attributes["activitypub.lookup.kind"],
|
|
734
|
+
"actor",
|
|
735
|
+
);
|
|
736
|
+
deepStrictEqual(
|
|
737
|
+
counters[0].attributes["activitypub.remote.host"],
|
|
738
|
+
"example.com",
|
|
739
|
+
);
|
|
740
|
+
// No `activitypub.document.fetch` measurement should be recorded by
|
|
741
|
+
// lookupObject itself; that lives on the document-loader path.
|
|
742
|
+
deepStrictEqual(
|
|
743
|
+
recorder.getMeasurements("activitypub.document.fetch").length,
|
|
744
|
+
0,
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
await t.step("records kind=object for a non-actor object", async () => {
|
|
749
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
750
|
+
const object = await lookupObject("https://example.com/object", {
|
|
751
|
+
documentLoader: mockDocumentLoader,
|
|
752
|
+
contextLoader: mockDocumentLoader,
|
|
753
|
+
meterProvider,
|
|
754
|
+
});
|
|
755
|
+
assertInstanceOf(object, Object);
|
|
756
|
+
const counters = recorder.getMeasurements("activitypub.object.lookup");
|
|
757
|
+
deepStrictEqual(counters.length, 1);
|
|
758
|
+
deepStrictEqual(
|
|
759
|
+
counters[0].attributes["activitypub.lookup.kind"],
|
|
760
|
+
"object",
|
|
761
|
+
);
|
|
762
|
+
deepStrictEqual(
|
|
763
|
+
counters[0].attributes["activitypub.remote.host"],
|
|
764
|
+
"example.com",
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
await t.step("records kind=other on null result", async () => {
|
|
769
|
+
fetchMock.removeRoutes();
|
|
770
|
+
fetchMock.get("begin:https://example.com/.well-known/webfinger", {
|
|
771
|
+
subject: "acct:janedoe@example.com",
|
|
772
|
+
links: [
|
|
773
|
+
{
|
|
774
|
+
rel: "self",
|
|
775
|
+
href: "https://example.com/404",
|
|
776
|
+
type: "application/activity+json",
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
});
|
|
780
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
781
|
+
const result = await lookupObject("janedoe@example.com", {
|
|
782
|
+
documentLoader: mockDocumentLoader,
|
|
783
|
+
contextLoader: mockDocumentLoader,
|
|
784
|
+
meterProvider,
|
|
785
|
+
});
|
|
786
|
+
deepStrictEqual(result, null);
|
|
787
|
+
const counters = recorder.getMeasurements("activitypub.object.lookup");
|
|
788
|
+
deepStrictEqual(counters.length, 1);
|
|
789
|
+
deepStrictEqual(
|
|
790
|
+
counters[0].attributes["activitypub.lookup.kind"],
|
|
791
|
+
"other",
|
|
792
|
+
);
|
|
793
|
+
deepStrictEqual(
|
|
794
|
+
counters[0].attributes["activitypub.remote.host"],
|
|
795
|
+
"example.com",
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
await t.step(
|
|
800
|
+
"omits counter when no meterProvider is provided",
|
|
801
|
+
async () => {
|
|
802
|
+
const [_unused, recorder] = createTestMeterProvider();
|
|
803
|
+
await lookupObject("https://example.com/object", {
|
|
804
|
+
documentLoader: mockDocumentLoader,
|
|
805
|
+
contextLoader: mockDocumentLoader,
|
|
806
|
+
});
|
|
807
|
+
deepStrictEqual(
|
|
808
|
+
recorder.getMeasurements("activitypub.object.lookup").length,
|
|
809
|
+
0,
|
|
810
|
+
);
|
|
811
|
+
},
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
await t.step(
|
|
815
|
+
"extracts host from a URL acct: instance",
|
|
816
|
+
async () => {
|
|
817
|
+
fetchMock.removeRoutes();
|
|
818
|
+
fetchMock.get("begin:https://example.com/.well-known/webfinger", {
|
|
819
|
+
subject: "acct:johndoe@example.com",
|
|
820
|
+
links: [
|
|
821
|
+
{
|
|
822
|
+
rel: "self",
|
|
823
|
+
href: "https://example.com/person",
|
|
824
|
+
type: "application/activity+json",
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
});
|
|
828
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
829
|
+
await lookupObject(new URL("acct:johndoe@example.com"), {
|
|
830
|
+
documentLoader: mockDocumentLoader,
|
|
831
|
+
contextLoader: mockDocumentLoader,
|
|
832
|
+
meterProvider,
|
|
833
|
+
});
|
|
834
|
+
const counter = recorder.getMeasurement("activitypub.object.lookup");
|
|
835
|
+
deepStrictEqual(
|
|
836
|
+
counter?.attributes["activitypub.remote.host"],
|
|
837
|
+
"example.com",
|
|
838
|
+
);
|
|
839
|
+
},
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
await t.step(
|
|
843
|
+
"omits host when the identifier carries path/query characters",
|
|
844
|
+
async () => {
|
|
845
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
846
|
+
// The bare-handle parser must refuse to put a path-bearing string
|
|
847
|
+
// like "example.com/leak" into the metric attribute.
|
|
848
|
+
await lookupObject("johndoe@example.com/leak", {
|
|
849
|
+
documentLoader: mockDocumentLoader,
|
|
850
|
+
contextLoader: mockDocumentLoader,
|
|
851
|
+
meterProvider,
|
|
852
|
+
});
|
|
853
|
+
const counter = recorder.getMeasurement("activitypub.object.lookup");
|
|
854
|
+
ok(counter != null);
|
|
855
|
+
deepStrictEqual(
|
|
856
|
+
"activitypub.remote.host" in counter.attributes,
|
|
857
|
+
false,
|
|
858
|
+
"high-cardinality handle suffixes must not be recorded as remote.host",
|
|
859
|
+
);
|
|
860
|
+
},
|
|
861
|
+
);
|
|
862
|
+
} finally {
|
|
863
|
+
fetchMock.removeRoutes();
|
|
864
|
+
fetchMock.hardReset();
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
|
|
696
868
|
// cSpell: ignore gildong
|
package/src/lookup.ts
CHANGED
|
@@ -6,13 +6,95 @@ import {
|
|
|
6
6
|
} from "@fedify/vocab-runtime";
|
|
7
7
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type Attributes,
|
|
11
|
+
type Counter,
|
|
12
|
+
type MeterProvider,
|
|
13
|
+
SpanStatusCode,
|
|
14
|
+
trace,
|
|
15
|
+
type TracerProvider,
|
|
16
|
+
} from "@opentelemetry/api";
|
|
10
17
|
import { delay } from "es-toolkit";
|
|
11
18
|
import metadata from "../deno.json" with { type: "json" };
|
|
19
|
+
import { isActor } from "./actor.ts";
|
|
12
20
|
import { toAcctUrl } from "./handle.ts";
|
|
13
21
|
import { getTypeId } from "./type.ts";
|
|
14
22
|
import { type Collection, type Link, Object } from "./vocab.ts";
|
|
15
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The classification of an ActivityStreams lookup performed by
|
|
26
|
+
* {@link lookupObject}, recorded as `activitypub.lookup.kind` on the
|
|
27
|
+
* `activitypub.object.lookup` counter.
|
|
28
|
+
*
|
|
29
|
+
* - `actor`: the resolved object is an {@link import("./actor.ts").Actor}.
|
|
30
|
+
* - `object`: the resolved object is a non-actor ActivityStreams object.
|
|
31
|
+
* - `other`: the lookup did not resolve to an object (not found, network
|
|
32
|
+
* failure, parse failure).
|
|
33
|
+
* @since 2.3.0
|
|
34
|
+
*/
|
|
35
|
+
export type ObjectLookupKind = "actor" | "object" | "other";
|
|
36
|
+
|
|
37
|
+
const objectLookupCounters = new WeakMap<MeterProvider, Counter>();
|
|
38
|
+
|
|
39
|
+
function getObjectLookupCounter(meterProvider: MeterProvider): Counter {
|
|
40
|
+
let counter = objectLookupCounters.get(meterProvider);
|
|
41
|
+
if (counter == null) {
|
|
42
|
+
counter = meterProvider
|
|
43
|
+
.getMeter(metadata.name, metadata.version)
|
|
44
|
+
.createCounter("activitypub.object.lookup", {
|
|
45
|
+
description:
|
|
46
|
+
"ActivityStreams object lookups via lookupObject(), classified " +
|
|
47
|
+
"by whether the resolved value is an Actor.",
|
|
48
|
+
unit: "{lookup}",
|
|
49
|
+
});
|
|
50
|
+
objectLookupCounters.set(meterProvider, counter);
|
|
51
|
+
}
|
|
52
|
+
return counter;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getLookupRemoteHost(identifier: string | URL): string | undefined {
|
|
56
|
+
let url: URL | undefined;
|
|
57
|
+
if (identifier instanceof URL) {
|
|
58
|
+
url = identifier;
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
url = new URL(identifier);
|
|
62
|
+
} catch {
|
|
63
|
+
// Not a URL — try to interpret as a bare fediverse handle below.
|
|
64
|
+
const stripped = identifier.startsWith("@")
|
|
65
|
+
? identifier.slice(1)
|
|
66
|
+
: identifier;
|
|
67
|
+
return extractHandleHost(stripped);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (url.hostname !== "") return url.hostname;
|
|
71
|
+
// `acct:` URIs are opaque (no `//host` form), so the URL hostname is
|
|
72
|
+
// empty. The user and authority live in `url.pathname` as
|
|
73
|
+
// `user@host`; reuse the same handle-extraction logic, which both
|
|
74
|
+
// takes only the substring after the last `@` and refuses to record
|
|
75
|
+
// anything that looks like a path / query / fragment rather than a
|
|
76
|
+
// bare hostname.
|
|
77
|
+
if (url.protocol === "acct:") return extractHandleHost(url.pathname);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractHandleHost(handle: string): string | undefined {
|
|
82
|
+
const at = handle.lastIndexOf("@");
|
|
83
|
+
if (at < 0 || at >= handle.length - 1) return undefined;
|
|
84
|
+
const candidate = handle.slice(at + 1);
|
|
85
|
+
// Reject anything that is not just an authority — paths, queries,
|
|
86
|
+
// fragments, and spaces would all leak high-cardinality metadata into
|
|
87
|
+
// the metric attribute, so we drop the host entirely in those cases.
|
|
88
|
+
if (/[/?#\s]/.test(candidate)) return undefined;
|
|
89
|
+
// Round-trip through `URL` so the parser validates the authority and
|
|
90
|
+
// strips any port/userinfo before we record it.
|
|
91
|
+
try {
|
|
92
|
+
return new URL(`https://${candidate}`).hostname || undefined;
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
16
98
|
const logger = getLogger(["fedify", "vocab", "lookup"]);
|
|
17
99
|
|
|
18
100
|
/**
|
|
@@ -67,6 +149,15 @@ export interface LookupObjectOptions {
|
|
|
67
149
|
*/
|
|
68
150
|
tracerProvider?: TracerProvider;
|
|
69
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The OpenTelemetry meter provider used to record the
|
|
154
|
+
* `activitypub.object.lookup` counter. If omitted, the counter is not
|
|
155
|
+
* emitted at all (the helper is opt-in to avoid touching the global
|
|
156
|
+
* meter provider for callers that do not use OpenTelemetry).
|
|
157
|
+
* @since 2.3.0
|
|
158
|
+
*/
|
|
159
|
+
meterProvider?: MeterProvider;
|
|
160
|
+
|
|
70
161
|
/**
|
|
71
162
|
* AbortSignal for cancelling the request.
|
|
72
163
|
* @since 1.8.0
|
|
@@ -118,8 +209,16 @@ export async function lookupObject(
|
|
|
118
209
|
return await tracer.startActiveSpan(
|
|
119
210
|
"activitypub.lookup_object",
|
|
120
211
|
async (span) => {
|
|
212
|
+
let kind: ObjectLookupKind = "other";
|
|
121
213
|
try {
|
|
122
214
|
const result = await lookupObjectInternal(identifier, options);
|
|
215
|
+
// Classify the result as soon as `lookupObjectInternal` returns,
|
|
216
|
+
// so that any subsequent throw (for example from
|
|
217
|
+
// `result.toJsonLd(options)` while building the span event) does
|
|
218
|
+
// not roll `kind` back to `"other"` in the `finally` block.
|
|
219
|
+
if (result != null) {
|
|
220
|
+
kind = isActor(result) ? "actor" : "object";
|
|
221
|
+
}
|
|
123
222
|
if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
124
223
|
else {
|
|
125
224
|
if (result.id != null) {
|
|
@@ -150,6 +249,14 @@ export async function lookupObject(
|
|
|
150
249
|
});
|
|
151
250
|
throw error;
|
|
152
251
|
} finally {
|
|
252
|
+
if (options.meterProvider != null) {
|
|
253
|
+
const attributes: Attributes = {
|
|
254
|
+
"activitypub.lookup.kind": kind,
|
|
255
|
+
};
|
|
256
|
+
const host = getLookupRemoteHost(identifier);
|
|
257
|
+
if (host != null) attributes["activitypub.remote.host"] = host;
|
|
258
|
+
getObjectLookupCounter(options.meterProvider).add(1, attributes);
|
|
259
|
+
}
|
|
153
260
|
span.end();
|
|
154
261
|
}
|
|
155
262
|
},
|