@fedify/fedify 2.4.0-dev.2077 → 2.4.0-dev.2093
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/dist/{builder-i3jxVe9m.mjs → builder-Ld4Y9ymR.mjs} +34 -3
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-Cayy-ECj.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
- package/dist/{deno-Djkzd-G_.mjs → deno-8t8k_6Yv.mjs} +1 -1
- package/dist/{docloader-dA0RswUV.mjs → docloader-DJKcsAiF.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +254 -5
- package/dist/federation/portable-gateway-keys.test.mjs +72 -11
- package/dist/federation/portable-inbox.test.mjs +513 -6
- package/dist/federation/portable.test.mjs +182 -4
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +89 -1
- package/dist/{federation-CFd0Jhhv.cjs → federation-aSHO04ob.cjs} +472 -74
- package/dist/{http-CCJ97Jql.mjs → http-B2ftY0Ha.mjs} +4 -4
- package/dist/{http-Dvy_IwRa.cjs → http-DSaSAN7X.cjs} +1609 -1457
- package/dist/{http-aqh3yFdM.js → http-DyT7DP1Y.js} +1518 -1390
- package/dist/{key-IxTTC8Yf.mjs → key-Dwm4VswT.mjs} +39 -99
- package/dist/{kv-cache-BP69NV0P.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
- package/dist/{kv-cache-Bwwbh-xR.mjs → kv-cache-D0yYukqk.mjs} +1 -1
- package/dist/{kv-cache-Y6SDRSX6.js → kv-cache-DmroAz3M.js} +1 -1
- package/dist/{ld-l0B9nREb.mjs → ld-BYZOoDzZ.mjs} +3 -3
- package/dist/{metrics-D1p6hv4z.mjs → metrics-B7YwKTFr.mjs} +1 -1
- package/dist/{middleware-BDjCUHMS.mjs → middleware-BNeqKSsL.mjs} +1 -1
- package/dist/{middleware-SSfryVzY.js → middleware-Blq0882d.js} +480 -82
- package/dist/{middleware-BMgzqBtr.mjs → middleware-E_0IH72m.mjs} +278 -80
- package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
- package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-DGbYmXqU.mjs → owner-Cgx2Dc7I.mjs} +5 -4
- package/dist/{owner-jH1z_YfD.js → owner-DAGSC5gX.js} +2 -2
- package/dist/{owner-BpOxL5PG.cjs → owner-szIZl4qI.cjs} +2 -2
- package/dist/{portable-key-CCwJGSsU.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-CVL7JnZd.mjs → proof-DVXC6nYz.mjs} +59 -18
- package/dist/{send-Ejy1jOz9.mjs → send-D-hOvYKy.mjs} +3 -3
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +160 -3
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +69 -5
- package/dist/sig/portable-key.test.mjs +176 -10
- package/dist/sig/proof.test.mjs +107 -4
- package/dist/{sig-Dpy1B1Ad.js → sig-B5Ell7pO.js} +19 -3
- package/dist/{sig-xoqy3ATq.cjs → sig-CXOKN4Br.cjs} +19 -3
- package/dist/{temporal-DNJ1cNpr.mjs → temporal-CqpKGEPk.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
|
@@ -2,16 +2,17 @@ import { Temporal } from "temporal-polyfill";
|
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
|
|
4
4
|
import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
|
|
5
|
-
import { $ as
|
|
5
|
+
import { $ as recordFanoutRecipients, A as verifyCompactJsonLd, B as validateCryptoKey, C as detachSignature, D as isClearlyMalformedContextReference, E as hasSignatureLike, G as instrumentDocumentLoader, H as getDurationMs, J as recordCollectionDispatchDuration, K as isAbortError, N as wrapContextLoaderForJsonLd, O as isInvalidUrlTypeError, P as exportJwk, T as hasSignature, U as getFederationMetrics, W as getRemoteHost, X as recordCollectionRequest, Y as recordCollectionPageItems, Z as recordCollectionTotalItems, _ as normalizeOutgoingActivityJsonLd, a as verifyRequestDetailed, at as preloadedOnlyDocumentLoader, d as verifyMapLocalProof, dt as getGatewayKeyBase, et as recordInboxActivity, f as verifyObject, ft as getPortableDid, gt as isPortableUri, h as verifyPortableObjectProofWithRoot, ht as isPortableId, i as verifyRequest, it as registerQueueDepthGauge, k as signJsonLd, l as hasProofLike, m as verifyPortableObjectProofPolicy, mt as isCompatibleKeyId, n as parseRfc9421SignatureInput, nt as recordOutboxEnqueue, ot as formatAcceptSignature, p as verifyPortableObjectProof, pt as hasPortableActor, q as recordCircuitBreakerStateChange, rt as recordWebFingerHandle, s as InvalidPortableObjectIdError, t as doubleKnock, tt as recordOutboxActivity, u as signObject, ut as getCanonicalPortableId, v as InvalidContextReferenceError, vt as name, w as getNormalizationContextLoader, x as compactJsonLd, y as assertSafeJsonLd, yt as version, z as importJwk } from "./http-DyT7DP1Y.js";
|
|
6
6
|
import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-C_cNlNVK.js";
|
|
7
|
-
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-
|
|
8
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-
|
|
7
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DAGSC5gX.js";
|
|
8
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DmroAz3M.js";
|
|
9
9
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
10
10
|
import { Router, RouterError, assertPath } from "@fedify/uri-template";
|
|
11
11
|
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
12
12
|
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
13
|
+
import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
14
|
+
import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
13
15
|
import { encodeHex } from "byte-encodings/hex";
|
|
14
|
-
import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, haveSameFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
15
16
|
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
16
17
|
import { uniq } from "es-toolkit";
|
|
17
18
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
@@ -205,10 +206,11 @@ var FederationBuilderImpl = class {
|
|
|
205
206
|
"federation",
|
|
206
207
|
"actor"
|
|
207
208
|
]);
|
|
208
|
-
const portable = actor.id
|
|
209
|
+
const portable = actor.id != null && isPortableId(actor.id);
|
|
209
210
|
if (actor.id == null) logger.warn("Actor dispatcher returned an actor without an id property. Set the property with Context.getActorUri(identifier).");
|
|
210
211
|
else if (!portable && actor.id.href != context.getActorUri(identifier).href) logger.warn("Actor dispatcher returned an actor with an id property that does not match the actor URI. Set the property with Context.getActorUri(identifier).");
|
|
211
212
|
if (actor instanceof Tombstone) return actor;
|
|
213
|
+
if (actor.id != null && isCompatibleEf61Iri(actor.id)) warnCompatibleActorId(actor.id, actor.gateway);
|
|
212
214
|
if (this.followingCallbacks != null && this.followingCallbacks.dispatcher != null) {
|
|
213
215
|
if (actor.followingId == null) logger.warn("You configured a following collection dispatcher, but the actor does not have a following property. Set the property with Context.getFollowingUri(identifier).");
|
|
214
216
|
else if (!portable && actor.followingId.href != context.getFollowingUri(identifier).href) logger.warn("You configured a following collection dispatcher, but the actor's following property does not match the following collection URI. Set the property with Context.getFollowingUri(identifier).");
|
|
@@ -733,6 +735,33 @@ var FederationBuilderImpl = class {
|
|
|
733
735
|
function createFederationBuilder() {
|
|
734
736
|
return new FederationBuilderImpl();
|
|
735
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Warns about an FEP-ef61 portable actor identified by a compatible identifier
|
|
740
|
+
* that is malformed, or is not on the actor's first gateway, where FEP-ef61
|
|
741
|
+
* requires publishers to construct compatible identifiers.
|
|
742
|
+
*/
|
|
743
|
+
function warnCompatibleActorId(actorId, gateway) {
|
|
744
|
+
const logger = getLogger([
|
|
745
|
+
"fedify",
|
|
746
|
+
"federation",
|
|
747
|
+
"actor"
|
|
748
|
+
]);
|
|
749
|
+
try {
|
|
750
|
+
fromCompatibleEf61Id(actorId);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (!(error instanceof TypeError)) throw error;
|
|
753
|
+
logger.warn("Actor dispatcher returned an actor whose id property, {actorId}, is a malformed FEP-ef61 compatible identifier: {error}", {
|
|
754
|
+
actorId: actorId.href,
|
|
755
|
+
error
|
|
756
|
+
});
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (gateway == null || !isGatewayUrl(gateway)) logger.warn("Actor dispatcher returned an actor whose id property, {actorId}, is an FEP-ef61 compatible identifier, but whose first gateway is not an HTTP(S) origin. Set the gateways property, and construct the compatible identifier with its first gateway.", { actorId: actorId.href });
|
|
760
|
+
else if (actorId.origin !== gateway.origin) logger.warn("Actor dispatcher returned an actor whose id property, {actorId}, is an FEP-ef61 compatible identifier on another gateway than its first gateway, {gateway}. FEP-ef61 requires publishers to construct compatible identifiers with the first gateway in the actor's gateways.", {
|
|
761
|
+
actorId: actorId.href,
|
|
762
|
+
gateway: gateway.origin
|
|
763
|
+
});
|
|
764
|
+
}
|
|
736
765
|
//#endregion
|
|
737
766
|
//#region src/federation/circuit-breaker.ts
|
|
738
767
|
const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
|
|
@@ -1309,7 +1338,14 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
|
|
|
1309
1338
|
//#endregion
|
|
1310
1339
|
//#region src/sig/compound-proof.ts
|
|
1311
1340
|
const textEncoder = new TextEncoder();
|
|
1312
|
-
|
|
1341
|
+
/**
|
|
1342
|
+
* Checks whether a literal `id` or `@id` value makes a map a portable object:
|
|
1343
|
+
* an `ap:` or `ap+ef61:` URI, or anything that looks like an FEP-ef61
|
|
1344
|
+
* compatible identifier, even a malformed one, which then fails the policy.
|
|
1345
|
+
*/
|
|
1346
|
+
function isPortableIdValue(value) {
|
|
1347
|
+
return typeof value === "string" && isPortableId(value);
|
|
1348
|
+
}
|
|
1313
1349
|
function isJsonMap(value) {
|
|
1314
1350
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
1315
1351
|
}
|
|
@@ -1555,18 +1591,20 @@ function collectPortableObjects(snapshot) {
|
|
|
1555
1591
|
for (let index = current.value.length - 1; index >= 0; index--) pending.push({
|
|
1556
1592
|
value: current.value[index],
|
|
1557
1593
|
path: childPath(current.path, String(index)),
|
|
1558
|
-
depth: current.depth + 1
|
|
1594
|
+
depth: current.depth + 1,
|
|
1595
|
+
keyOf: current.keyOf
|
|
1559
1596
|
});
|
|
1560
1597
|
continue;
|
|
1561
1598
|
}
|
|
1562
1599
|
if (!isCompoundProofJsonObject(current.value)) continue;
|
|
1563
1600
|
const document = current.value;
|
|
1564
|
-
const id = [document.id, document["@id"]].find(
|
|
1601
|
+
const id = [document.id, document["@id"]].find(isPortableIdValue);
|
|
1565
1602
|
if (typeof id === "string") objects.push(Object.freeze({
|
|
1566
1603
|
path: current.path,
|
|
1567
1604
|
id,
|
|
1568
1605
|
depth: current.depth,
|
|
1569
|
-
document
|
|
1606
|
+
document,
|
|
1607
|
+
...current.keyOf == null ? {} : { keyOf: current.keyOf }
|
|
1570
1608
|
}));
|
|
1571
1609
|
const entries = Object.entries(document).sort(([left], [right]) => comparePaths(left, right));
|
|
1572
1610
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
@@ -1575,7 +1613,11 @@ function collectPortableObjects(snapshot) {
|
|
|
1575
1613
|
pending.push({
|
|
1576
1614
|
value,
|
|
1577
1615
|
path: childPath(current.path, key),
|
|
1578
|
-
depth: current.depth + 1
|
|
1616
|
+
depth: current.depth + 1,
|
|
1617
|
+
...typeof id === "string" && (key === "publicKey" || key === "assertionMethod") ? { keyOf: {
|
|
1618
|
+
path: current.path,
|
|
1619
|
+
member: key
|
|
1620
|
+
} } : {}
|
|
1579
1621
|
});
|
|
1580
1622
|
}
|
|
1581
1623
|
}
|
|
@@ -1583,8 +1625,9 @@ function collectPortableObjects(snapshot) {
|
|
|
1583
1625
|
return objects;
|
|
1584
1626
|
}
|
|
1585
1627
|
/**
|
|
1586
|
-
* Determines whether a received JSON tree contains a portable object ID
|
|
1587
|
-
*
|
|
1628
|
+
* Determines whether a received JSON tree contains a portable object ID, i.e.,
|
|
1629
|
+
* an `ap:` or `ap+ef61:` URI or an FEP-ef61 compatible identifier, outside
|
|
1630
|
+
* a proof or context definition, without exceeding discovery limits.
|
|
1588
1631
|
*
|
|
1589
1632
|
* This is only an applicability check. It does not authenticate the input;
|
|
1590
1633
|
* callers must still use {@link verifyCompoundPortableObjectProofs} to obtain
|
|
@@ -1631,7 +1674,7 @@ function inspectCompoundPortableObjectApplicability(json, limits) {
|
|
|
1631
1674
|
} else if (isJsonMap(value)) {
|
|
1632
1675
|
mapCount++;
|
|
1633
1676
|
if (mapCount > limits.maxMaps) return "indeterminate";
|
|
1634
|
-
if ([value.id, value["@id"]].some(
|
|
1677
|
+
if ([value.id, value["@id"]].some(isPortableIdValue)) return "present";
|
|
1635
1678
|
if (!addByteLength(2)) return "indeterminate";
|
|
1636
1679
|
const children = function* () {
|
|
1637
1680
|
let first = true;
|
|
@@ -1694,7 +1737,8 @@ function findInJsonMaps(json, visit) {
|
|
|
1694
1737
|
/**
|
|
1695
1738
|
* Determines whether an outgoing JSON tree contains portable content that
|
|
1696
1739
|
* requires producer-side proof-shape checks: a map, outside proof and context
|
|
1697
|
-
* values, whose direct `id` or `@id` is a portable ActivityPub URI
|
|
1740
|
+
* values, whose direct `id` or `@id` is a portable ActivityPub URI or an
|
|
1741
|
+
* FEP-ef61 compatible identifier.
|
|
1698
1742
|
*
|
|
1699
1743
|
* This deliberately differs from
|
|
1700
1744
|
* {@link inspectCompoundPortableObjectApplicability}: it applies no resource
|
|
@@ -1707,7 +1751,7 @@ function findInJsonMaps(json, visit) {
|
|
|
1707
1751
|
* @internal
|
|
1708
1752
|
*/
|
|
1709
1753
|
function containsCompoundPortableObject(json) {
|
|
1710
|
-
return findInJsonMaps(json, (map) => [map.id, map["@id"]].some(
|
|
1754
|
+
return findInJsonMaps(json, (map) => [map.id, map["@id"]].some(isPortableIdValue) ? true : void 0) ?? false;
|
|
1711
1755
|
}
|
|
1712
1756
|
/**
|
|
1713
1757
|
* Finds the first direct `proof` member whose value the map-local
|
|
@@ -1804,7 +1848,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
|
|
|
1804
1848
|
if (discovered.status !== "ok") return discovered;
|
|
1805
1849
|
const proofs = await verifyDiscoveredProofDocuments(discovered, options);
|
|
1806
1850
|
const proofByPath = new Map(proofs.map((proof) => [proof.path, proof]));
|
|
1807
|
-
const
|
|
1851
|
+
const verifyPortableObject = async (object) => {
|
|
1808
1852
|
const metadata = {
|
|
1809
1853
|
path: object.path,
|
|
1810
1854
|
id: object.id,
|
|
@@ -1836,16 +1880,174 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
|
|
|
1836
1880
|
reason: { type: "invalidPortableObject" }
|
|
1837
1881
|
});
|
|
1838
1882
|
}
|
|
1883
|
+
};
|
|
1884
|
+
const collected = collectPortableObjects(discovered.snapshot);
|
|
1885
|
+
const objectByPath = new Map(collected.map((object) => [object.path, object]));
|
|
1886
|
+
const isKeyCandidate = (object) => object.keyOf != null && !Object.hasOwn(object.document, "proof");
|
|
1887
|
+
const results = /* @__PURE__ */ new Map();
|
|
1888
|
+
for (const result of await Promise.all(collected.filter((object) => !isKeyCandidate(object)).map(verifyPortableObject))) results.set(result.path, result);
|
|
1889
|
+
let projections = 0;
|
|
1890
|
+
for (const object of collected) {
|
|
1891
|
+
if (!isKeyCandidate(object)) continue;
|
|
1892
|
+
const { path, member } = object.keyOf;
|
|
1893
|
+
const parent = results.get(path);
|
|
1894
|
+
const parentObject = objectByPath.get(path);
|
|
1895
|
+
if (parent?.verified === true && parentObject != null && projections++ < MAX_KEY_PROJECTIONS && await isEmbeddedPortableKey(parentObject.document, member, object.document, parent.id)) continue;
|
|
1896
|
+
results.set(object.path, await verifyPortableObject(object));
|
|
1897
|
+
}
|
|
1898
|
+
const portableObjects = Object.freeze(collected.flatMap((object) => {
|
|
1899
|
+
const result = results.get(object.path);
|
|
1900
|
+
return result == null ? [] : [result];
|
|
1839
1901
|
}));
|
|
1840
1902
|
return {
|
|
1841
1903
|
status: "ok",
|
|
1842
1904
|
verified: portableObjects.length > 0 && proofs.every((proof) => proof.verified) && portableObjects.every((object) => object.verified),
|
|
1843
1905
|
snapshot: discovered.snapshot,
|
|
1844
1906
|
proofs,
|
|
1845
|
-
portableObjects
|
|
1907
|
+
portableObjects,
|
|
1846
1908
|
statistics: discovered.statistics
|
|
1847
1909
|
};
|
|
1848
1910
|
}
|
|
1911
|
+
/**
|
|
1912
|
+
* The maximum number of embedded keys per compound document that are checked
|
|
1913
|
+
* by {@link isEmbeddedPortableKey}, each of which expands a projection of its
|
|
1914
|
+
* parent. Keys beyond it have to carry their own proofs.
|
|
1915
|
+
*/
|
|
1916
|
+
const MAX_KEY_PROJECTIONS = 64;
|
|
1917
|
+
const SECURITY = "https://w3id.org/security#";
|
|
1918
|
+
const KEY_TYPES = /* @__PURE__ */ new Set([`${SECURITY}Key`, `${SECURITY}Multikey`]);
|
|
1919
|
+
const KEY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1920
|
+
"@id",
|
|
1921
|
+
"@type",
|
|
1922
|
+
`${SECURITY}owner`,
|
|
1923
|
+
`${SECURITY}controller`,
|
|
1924
|
+
`${SECURITY}publicKeyPem`,
|
|
1925
|
+
`${SECURITY}publicKeyMultibase`
|
|
1926
|
+
]);
|
|
1927
|
+
/**
|
|
1928
|
+
* Tells whether an unsigned map embedded in a verified portable map is just
|
|
1929
|
+
* a key of the portable object the parent is, e.g., a gateway key of
|
|
1930
|
+
* a portable actor, which the parent's proof covers.
|
|
1931
|
+
*
|
|
1932
|
+
* The key's meaning depends on the contexts active where it sits, so it is
|
|
1933
|
+
* decided on a projection of the parent that keeps only what those contexts
|
|
1934
|
+
* depend on: the parent's `@context`, its types and ID, and the member that
|
|
1935
|
+
* holds the key. That holds only if nothing else in the parent can change
|
|
1936
|
+
* them, so both documents may only use contexts that define no keyword
|
|
1937
|
+
* aliases and no scoped contexts; Fedify's preloaded contexts, the only
|
|
1938
|
+
* remote ones the compound policy loads, alias just `id` and `type`.
|
|
1939
|
+
*
|
|
1940
|
+
* The projection must expand to the parent's verified ID with the key as the
|
|
1941
|
+
* single value of `publicKey` or `assertionMethod`, and the key must be
|
|
1942
|
+
* nothing but a `CryptographicKey` or `Multikey` whose ID is a compatible
|
|
1943
|
+
* identifier of the parent plus a fragment, and whose owner or controller,
|
|
1944
|
+
* if any, is the parent.
|
|
1945
|
+
*/
|
|
1946
|
+
async function isEmbeddedPortableKey(parent, member, key, parentId) {
|
|
1947
|
+
const parentPortableId = getCanonicalPortableIdOf(parentId);
|
|
1948
|
+
if (parentPortableId == null) return false;
|
|
1949
|
+
if (!isSimpleContext(parent["@context"]) || Object.hasOwn(key, "@context") && !isSimpleContext(key["@context"])) return false;
|
|
1950
|
+
const literalIds = [key.id, key["@id"]].filter((id) => id !== void 0);
|
|
1951
|
+
if (literalIds.length < 1 || literalIds.some((id) => id !== literalIds[0]) || typeof literalIds[0] !== "string" || !isKeyIdOf(literalIds[0], parentPortableId)) return false;
|
|
1952
|
+
const projection = {};
|
|
1953
|
+
for (const property of [
|
|
1954
|
+
"@context",
|
|
1955
|
+
"type",
|
|
1956
|
+
"@type",
|
|
1957
|
+
"id",
|
|
1958
|
+
"@id"
|
|
1959
|
+
]) if (Object.hasOwn(parent, property)) projection[property] = parent[property];
|
|
1960
|
+
projection[member] = key;
|
|
1961
|
+
let expanded;
|
|
1962
|
+
try {
|
|
1963
|
+
expanded = await jsonld.expand(structuredClone(projection), {
|
|
1964
|
+
documentLoader: getNormalizationContextLoader(preloadedOnlyDocumentLoader),
|
|
1965
|
+
keepFreeFloatingNodes: true
|
|
1966
|
+
});
|
|
1967
|
+
} catch {
|
|
1968
|
+
return false;
|
|
1969
|
+
}
|
|
1970
|
+
if (expanded.length !== 1 || hasGraphKeyword(expanded)) return false;
|
|
1971
|
+
const node = expanded[0];
|
|
1972
|
+
if (!isJsonMap(node) || typeof node["@id"] !== "string" || getCanonicalPortableIdOf(node["@id"]) !== parentPortableId) return false;
|
|
1973
|
+
const properties = Object.keys(node).filter((property) => property !== "@id" && property !== "@type");
|
|
1974
|
+
if (properties.length !== 1 || properties[0] !== `${SECURITY}${member}`) return false;
|
|
1975
|
+
const values = node[properties[0]];
|
|
1976
|
+
if (!Array.isArray(values) || values.length !== 1) return false;
|
|
1977
|
+
const expandedKey = values[0];
|
|
1978
|
+
if (!isJsonMap(expandedKey) || expandedKey["@id"] !== literalIds[0] || Object.keys(expandedKey).some((property) => !KEY_PROPERTIES.has(property))) return false;
|
|
1979
|
+
const types = expandedKey["@type"];
|
|
1980
|
+
if (types !== void 0 && (!Array.isArray(types) || types.some((type) => typeof type !== "string" || !KEY_TYPES.has(type)))) return false;
|
|
1981
|
+
const pem = expandedKey[`${SECURITY}publicKeyPem`];
|
|
1982
|
+
const multibase = expandedKey[`${SECURITY}publicKeyMultibase`];
|
|
1983
|
+
if (pem === void 0 && multibase === void 0) return false;
|
|
1984
|
+
if (pem !== void 0 && !isSingleStringValue(pem)) return false;
|
|
1985
|
+
if (multibase !== void 0 && !isSingleStringValue(multibase, `${SECURITY}multibase`)) return false;
|
|
1986
|
+
for (const property of [`${SECURITY}owner`, `${SECURITY}controller`]) {
|
|
1987
|
+
const owners = expandedKey[property];
|
|
1988
|
+
if (owners === void 0) continue;
|
|
1989
|
+
if (!Array.isArray(owners) || owners.length !== 1 || !isJsonMap(owners[0]) || Object.keys(owners[0]).length !== 1 || typeof owners[0]["@id"] !== "string" || getCanonicalPortableIdOf(owners[0]["@id"]) !== parentPortableId) return false;
|
|
1990
|
+
}
|
|
1991
|
+
return true;
|
|
1992
|
+
}
|
|
1993
|
+
function getCanonicalPortableIdOf(id) {
|
|
1994
|
+
try {
|
|
1995
|
+
return getCanonicalPortableId(parseIri(id));
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
if (error instanceof TypeError) return null;
|
|
1998
|
+
throw error;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Checks whether an ID is a compatible identifier of the given portable
|
|
2003
|
+
* object plus a fragment.
|
|
2004
|
+
*/
|
|
2005
|
+
function isKeyIdOf(id, portableId) {
|
|
2006
|
+
let url;
|
|
2007
|
+
try {
|
|
2008
|
+
if (fromCompatibleEf61Id(id) == null) return false;
|
|
2009
|
+
url = new URL(id);
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
if (error instanceof TypeError) return false;
|
|
2012
|
+
throw error;
|
|
2013
|
+
}
|
|
2014
|
+
if (url.hash.length < 2) return false;
|
|
2015
|
+
url.hash = "";
|
|
2016
|
+
return getCanonicalPortableId(url) === portableId;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Checks whether a `@context` value defines no keyword aliases and no scoped
|
|
2020
|
+
* contexts: only references to contexts and term definitions that map terms
|
|
2021
|
+
* to IRIs, optionally with an `@id` or `@vocab` type coercion or a datatype.
|
|
2022
|
+
*/
|
|
2023
|
+
function isSimpleContext(context) {
|
|
2024
|
+
if (typeof context === "string") return true;
|
|
2025
|
+
if (Array.isArray(context)) return context.every(isSimpleContext);
|
|
2026
|
+
if (context === void 0 || !isCompoundProofJsonObject(context)) return false;
|
|
2027
|
+
return Object.entries(context).every(([term, definition]) => {
|
|
2028
|
+
if (term.startsWith("@")) return false;
|
|
2029
|
+
if (typeof definition === "string") return !definition.startsWith("@");
|
|
2030
|
+
if (!isCompoundProofJsonObject(definition)) return false;
|
|
2031
|
+
const { "@id": id, "@type": type, ...rest } = definition;
|
|
2032
|
+
return Object.keys(rest).length < 1 && typeof id === "string" && !id.startsWith("@") && (type === void 0 || type === "@id" || type === "@vocab" || typeof type === "string" && !type.startsWith("@"));
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
function hasGraphKeyword(value) {
|
|
2036
|
+
if (Array.isArray(value)) return value.some(hasGraphKeyword);
|
|
2037
|
+
if (!isJsonMap(value)) return false;
|
|
2038
|
+
return Object.entries(value).some(([key, child]) => key === "@graph" || key === "@included" || key === "@reverse" || hasGraphKeyword(child));
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Checks whether expanded values are a single plain string, optionally with
|
|
2042
|
+
* the given datatype.
|
|
2043
|
+
*/
|
|
2044
|
+
function isSingleStringValue(values, datatype) {
|
|
2045
|
+
if (!Array.isArray(values) || values.length !== 1) return false;
|
|
2046
|
+
const value = values[0];
|
|
2047
|
+
if (!isJsonMap(value) || typeof value["@value"] !== "string") return false;
|
|
2048
|
+
const keys = Object.keys(value);
|
|
2049
|
+
return keys.length === 1 || datatype != null && keys.length === 2 && value["@type"] === datatype;
|
|
2050
|
+
}
|
|
1849
2051
|
//#endregion
|
|
1850
2052
|
//#region src/federation/inbox.ts
|
|
1851
2053
|
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
@@ -2564,14 +2766,7 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
2564
2766
|
} });
|
|
2565
2767
|
}
|
|
2566
2768
|
function isRequestedPortableObject(object, canonicalId) {
|
|
2567
|
-
|
|
2568
|
-
if (object.id.protocol !== "ap:" && object.id.protocol !== "ap+ef61:") return false;
|
|
2569
|
-
try {
|
|
2570
|
-
return canonicalizePortableUri(formatIri(object.id)) === canonicalId;
|
|
2571
|
-
} catch (error) {
|
|
2572
|
-
if (error instanceof TypeError) return false;
|
|
2573
|
-
throw error;
|
|
2574
|
-
}
|
|
2769
|
+
return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
|
|
2575
2770
|
}
|
|
2576
2771
|
async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
2577
2772
|
const expanded = await jsonld.expand(jsonLd, {
|
|
@@ -2583,6 +2778,7 @@ async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
|
2583
2778
|
return typeof id === "string" ? id : null;
|
|
2584
2779
|
}
|
|
2585
2780
|
function isRequestedPortableId(id, canonicalId) {
|
|
2781
|
+
if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
|
|
2586
2782
|
try {
|
|
2587
2783
|
return canonicalizePortableUri(id) === canonicalId;
|
|
2588
2784
|
} catch (error) {
|
|
@@ -2633,13 +2829,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
|
2633
2829
|
* @param parameters The parameters for handling the collection.
|
|
2634
2830
|
* @returns A promise that resolves to an HTTP response.
|
|
2635
2831
|
*/
|
|
2636
|
-
async function handleCollection(request, { name: name$
|
|
2637
|
-
const spanName = name$
|
|
2832
|
+
async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
|
|
2833
|
+
const spanName = name$2.trim().replace(/\s+/g, "_");
|
|
2638
2834
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
2639
2835
|
const tracer = tracerProvider.getTracer(name, version);
|
|
2640
2836
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
2641
2837
|
const metricBase = {
|
|
2642
|
-
kind: getCollectionMetricKind(name$
|
|
2838
|
+
kind: getCollectionMetricKind(name$2),
|
|
2643
2839
|
page: cursor != null,
|
|
2644
2840
|
dispatcher: "built_in"
|
|
2645
2841
|
};
|
|
@@ -2680,7 +2876,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
2680
2876
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
2681
2877
|
return await onNotFound(request);
|
|
2682
2878
|
}
|
|
2683
|
-
const items = filterCollectionItems(page.items, name$
|
|
2879
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
2684
2880
|
itemCount = items.length;
|
|
2685
2881
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
2686
2882
|
return items;
|
|
@@ -2720,7 +2916,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
2720
2916
|
} else {
|
|
2721
2917
|
const uri = new URL(baseUri);
|
|
2722
2918
|
uri.searchParams.set("cursor", cursor);
|
|
2723
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$
|
|
2919
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
|
|
2724
2920
|
kind: SpanKind.SERVER,
|
|
2725
2921
|
attributes: {
|
|
2726
2922
|
"activitypub.collection.id": uri.href,
|
|
@@ -2736,7 +2932,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
2736
2932
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
2737
2933
|
return await onNotFound(request);
|
|
2738
2934
|
}
|
|
2739
|
-
const items = filterCollectionItems(page.items, name$
|
|
2935
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
2740
2936
|
itemCount = items.length;
|
|
2741
2937
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
2742
2938
|
return {
|
|
@@ -3445,7 +3641,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3445
3641
|
return await respondInvalidActivity(error);
|
|
3446
3642
|
}
|
|
3447
3643
|
activityVerified = true;
|
|
3448
|
-
if (!skipSignatureVerification &&
|
|
3644
|
+
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
3449
3645
|
proofVerified = await verifyObject(Activity, jsonWithoutSig, {
|
|
3450
3646
|
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3451
3647
|
documentLoader: ctx.documentLoader,
|
|
@@ -3629,7 +3825,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3629
3825
|
"http_signatures.verified": httpSigKey != null,
|
|
3630
3826
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
3631
3827
|
});
|
|
3632
|
-
if (!skipSignatureVerification && !proofVerified && (
|
|
3828
|
+
if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableGatewayKey(httpSigKey))) {
|
|
3633
3829
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3634
3830
|
logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
|
|
3635
3831
|
activity: json,
|
|
@@ -3647,6 +3843,32 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3647
3843
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3648
3844
|
});
|
|
3649
3845
|
}
|
|
3846
|
+
if (!skipSignatureVerification && activity.id != null && isPortableId(activity.id)) {
|
|
3847
|
+
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
3848
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3849
|
+
documentLoader: ctx.documentLoader,
|
|
3850
|
+
keyCache,
|
|
3851
|
+
meterProvider,
|
|
3852
|
+
tracerProvider
|
|
3853
|
+
});
|
|
3854
|
+
if (rejection != null) {
|
|
3855
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3856
|
+
logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
|
|
3857
|
+
activity: json,
|
|
3858
|
+
recipient,
|
|
3859
|
+
activityId: activity.id.href,
|
|
3860
|
+
reason: rejection
|
|
3861
|
+
});
|
|
3862
|
+
span.setStatus({
|
|
3863
|
+
code: SpanStatusCode.ERROR,
|
|
3864
|
+
message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
|
|
3865
|
+
});
|
|
3866
|
+
return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
|
|
3867
|
+
status: 401,
|
|
3868
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3650
3872
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
3651
3873
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3652
3874
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
@@ -3884,8 +4106,8 @@ var CustomCollectionHandler = class {
|
|
|
3884
4106
|
* @param CollectionPage The CollectionPage constructor.
|
|
3885
4107
|
* @param filterPredicate Optional filter predicate for items.
|
|
3886
4108
|
*/
|
|
3887
|
-
constructor(name$
|
|
3888
|
-
this.name = name$
|
|
4109
|
+
constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
|
|
4110
|
+
this.name = name$1;
|
|
3889
4111
|
this.values = values;
|
|
3890
4112
|
this.context = context;
|
|
3891
4113
|
this.callbacks = callbacks;
|
|
@@ -4376,7 +4598,41 @@ const MIN_COMPONENTS = [
|
|
|
4376
4598
|
* establish.
|
|
4377
4599
|
*/
|
|
4378
4600
|
function isPortableGatewayKey(key) {
|
|
4379
|
-
return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null &&
|
|
4601
|
+
return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
|
|
4602
|
+
}
|
|
4603
|
+
/**
|
|
4604
|
+
* Checks whether an activity needs an Object Integrity Proof by its DID:
|
|
4605
|
+
* either it is performed by an [FEP-ef61] portable actor, or its own ID is
|
|
4606
|
+
* portable. Both `ap:` URIs and compatible identifiers count as portable.
|
|
4607
|
+
*
|
|
4608
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
4609
|
+
*/
|
|
4610
|
+
function isPortableActivity(activity) {
|
|
4611
|
+
return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
|
|
4612
|
+
}
|
|
4613
|
+
/**
|
|
4614
|
+
* Verifies that a portable activity's own ID belongs to the DID that signed
|
|
4615
|
+
* it, on the JSON-LD document received. The ID the proof policy checks has
|
|
4616
|
+
* to be the parsed activity's ID, so that a remote context that changes
|
|
4617
|
+
* between loads cannot make the two name different objects.
|
|
4618
|
+
* @returns `null` if the ID is verified, or the reason why it is not.
|
|
4619
|
+
*/
|
|
4620
|
+
async function verifyPortableActivityId(activityId, jsonLd, options) {
|
|
4621
|
+
const expectedId = getCanonicalPortableId(activityId);
|
|
4622
|
+
if (expectedId == null) return "malformed portable ID";
|
|
4623
|
+
let verification;
|
|
4624
|
+
try {
|
|
4625
|
+
verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
|
|
4626
|
+
} catch (error) {
|
|
4627
|
+
if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
|
|
4628
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
4629
|
+
return "malformed document";
|
|
4630
|
+
}
|
|
4631
|
+
const { result, root } = verification;
|
|
4632
|
+
if (!result.verified) return result.reason.type;
|
|
4633
|
+
const rootId = root?.["@id"];
|
|
4634
|
+
if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
|
|
4635
|
+
return null;
|
|
4380
4636
|
}
|
|
4381
4637
|
//#endregion
|
|
4382
4638
|
//#region src/nodeinfo/handler.ts
|
|
@@ -4944,16 +5200,17 @@ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, loc
|
|
|
4944
5200
|
reason: "notFound"
|
|
4945
5201
|
};
|
|
4946
5202
|
const id = actor.id;
|
|
4947
|
-
|
|
5203
|
+
const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
|
|
5204
|
+
if (id == null || did == null) return {
|
|
4948
5205
|
status: "rejected",
|
|
4949
5206
|
reason: "notPortable"
|
|
4950
5207
|
};
|
|
4951
|
-
if (
|
|
5208
|
+
if (did !== authority) return {
|
|
4952
5209
|
status: "rejected",
|
|
4953
5210
|
reason: "authorityMismatch"
|
|
4954
5211
|
};
|
|
4955
|
-
const inboxId = actor.inboxId;
|
|
4956
|
-
if (inboxId == null ||
|
|
5212
|
+
const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
|
|
5213
|
+
if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
|
|
4957
5214
|
status: "rejected",
|
|
4958
5215
|
reason: "inboxMismatch"
|
|
4959
5216
|
};
|
|
@@ -4965,23 +5222,22 @@ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, loc
|
|
|
4965
5222
|
return {
|
|
4966
5223
|
status: "accepted",
|
|
4967
5224
|
recipient: {
|
|
5225
|
+
actorId: id,
|
|
4968
5226
|
inboxId,
|
|
4969
5227
|
canonicalInboxId,
|
|
4970
5228
|
gateways
|
|
4971
5229
|
}
|
|
4972
5230
|
};
|
|
4973
5231
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
}
|
|
4982
|
-
function canonicalize(iri) {
|
|
5232
|
+
/**
|
|
5233
|
+
* Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
|
|
5234
|
+
* identifier, keeping its query.
|
|
5235
|
+
* @returns The portable URI, or `null` if the ID is neither, or is malformed.
|
|
5236
|
+
*/
|
|
5237
|
+
function toPortableUri(id) {
|
|
5238
|
+
if (isPortableUri(id)) return id;
|
|
4983
5239
|
try {
|
|
4984
|
-
return
|
|
5240
|
+
return fromCompatibleEf61Id(id);
|
|
4985
5241
|
} catch (error) {
|
|
4986
5242
|
if (error instanceof TypeError) return null;
|
|
4987
5243
|
throw error;
|
|
@@ -4998,8 +5254,11 @@ function canonicalize(iri) {
|
|
|
4998
5254
|
* Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
|
|
4999
5255
|
* forwarded.
|
|
5000
5256
|
*
|
|
5001
|
-
* Forwarded requests are
|
|
5002
|
-
*
|
|
5257
|
+
* Forwarded requests are signed with HTTP Signatures by the RSA key among
|
|
5258
|
+
* {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
|
|
5259
|
+
* servers requiring HTTP Signatures accept them, and are sent unsigned
|
|
5260
|
+
* otherwise. Either way, the receiving gateways authenticate the activity by
|
|
5261
|
+
* its own proof.
|
|
5003
5262
|
* @param parameters The parameters.
|
|
5004
5263
|
* @returns The target inbox URLs that the activity was handed off to.
|
|
5005
5264
|
*/
|
|
@@ -5012,7 +5271,7 @@ async function forwardPortableInboxActivity(parameters) {
|
|
|
5012
5271
|
const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
|
|
5013
5272
|
const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
|
|
5014
5273
|
if (kv.cas == null || maxTargets < 1) return [];
|
|
5015
|
-
const canonicalActivityId =
|
|
5274
|
+
const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
|
|
5016
5275
|
const targets = getForwardingTargets(recipient, excludedOrigins);
|
|
5017
5276
|
if (targets.length > maxTargets) {
|
|
5018
5277
|
logger.warn("The portable actor that owns the inbox {inbox} has more than {max} other gateways; the activity {activityId} is forwarded only to the first {max} of them.", {
|
|
@@ -5071,20 +5330,59 @@ function getForwardingTargets(recipient, excludedOrigins) {
|
|
|
5071
5330
|
}
|
|
5072
5331
|
return targets;
|
|
5073
5332
|
}
|
|
5074
|
-
|
|
5333
|
+
/**
|
|
5334
|
+
* Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
|
|
5335
|
+
* key, as {@link sendActivity} signs requests only with such a key. Since
|
|
5336
|
+
* the targets have already been claimed, the activity is forwarded unsigned
|
|
5337
|
+
* rather than not at all if the key cannot be resolved or used.
|
|
5338
|
+
*/
|
|
5339
|
+
async function resolveForwardingKey({ getKeys, activityId }) {
|
|
5340
|
+
if (getKeys == null) return null;
|
|
5341
|
+
const logger = getLogger([
|
|
5342
|
+
"fedify",
|
|
5343
|
+
"federation",
|
|
5344
|
+
"inbox"
|
|
5345
|
+
]);
|
|
5346
|
+
let key;
|
|
5347
|
+
try {
|
|
5348
|
+
key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
|
|
5349
|
+
} catch (error) {
|
|
5350
|
+
logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
5351
|
+
activityId: activityId.href,
|
|
5352
|
+
error
|
|
5353
|
+
});
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
if (key == null) return null;
|
|
5357
|
+
try {
|
|
5358
|
+
validateCryptoKey(key.privateKey, "private");
|
|
5359
|
+
} catch (error) {
|
|
5360
|
+
if (!(error instanceof TypeError)) throw error;
|
|
5361
|
+
logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
5362
|
+
keyId: key.keyId.href,
|
|
5363
|
+
activityId: activityId.href,
|
|
5364
|
+
error
|
|
5365
|
+
});
|
|
5366
|
+
return null;
|
|
5367
|
+
}
|
|
5368
|
+
return key;
|
|
5369
|
+
}
|
|
5370
|
+
async function forwardImmediately(parameters, inboxes) {
|
|
5371
|
+
const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
|
|
5075
5372
|
const logger = getLogger([
|
|
5076
5373
|
"fedify",
|
|
5077
5374
|
"federation",
|
|
5078
5375
|
"inbox"
|
|
5079
5376
|
]);
|
|
5080
5377
|
const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
|
|
5081
|
-
const
|
|
5378
|
+
const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
|
|
5082
5379
|
activity,
|
|
5083
5380
|
activityId: activityId.href,
|
|
5084
5381
|
activityType,
|
|
5085
|
-
keys: [],
|
|
5382
|
+
keys: key == null ? [] : [key],
|
|
5086
5383
|
inbox,
|
|
5087
5384
|
allowPrivateAddress,
|
|
5385
|
+
specDeterminer,
|
|
5088
5386
|
meterProvider,
|
|
5089
5387
|
tracerProvider
|
|
5090
5388
|
}).catch((error) => {
|
|
@@ -5093,9 +5391,9 @@ async function forwardImmediately({ activity, activityId, activityType, allowPri
|
|
|
5093
5391
|
inbox: inbox.href,
|
|
5094
5392
|
error
|
|
5095
5393
|
});
|
|
5096
|
-
}));
|
|
5394
|
+
}))));
|
|
5097
5395
|
let timer;
|
|
5098
|
-
const timedOut = await Promise.race([
|
|
5396
|
+
const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
|
|
5099
5397
|
timer = setTimeout(() => resolve(true), deadline);
|
|
5100
5398
|
})]);
|
|
5101
5399
|
clearTimeout(timer);
|
|
@@ -5104,12 +5402,27 @@ async function forwardImmediately({ activity, activityId, activityType, allowPri
|
|
|
5104
5402
|
deadline
|
|
5105
5403
|
});
|
|
5106
5404
|
}
|
|
5107
|
-
async function enqueueForwarding(
|
|
5405
|
+
async function enqueueForwarding(parameters, outboxQueue, inboxes) {
|
|
5406
|
+
const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
|
|
5108
5407
|
const logger = getLogger([
|
|
5109
5408
|
"fedify",
|
|
5110
5409
|
"federation",
|
|
5111
5410
|
"inbox"
|
|
5112
5411
|
]);
|
|
5412
|
+
const key = await resolveForwardingKey(parameters);
|
|
5413
|
+
const keys = [];
|
|
5414
|
+
if (key != null) try {
|
|
5415
|
+
keys.push({
|
|
5416
|
+
keyId: key.keyId.href,
|
|
5417
|
+
privateKey: await exportJwk(key.privateKey)
|
|
5418
|
+
});
|
|
5419
|
+
} catch (error) {
|
|
5420
|
+
logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
5421
|
+
keyId: key.keyId.href,
|
|
5422
|
+
activityId: activityId.href,
|
|
5423
|
+
error
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5113
5426
|
startQueue?.();
|
|
5114
5427
|
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
5115
5428
|
const traceContext = {};
|
|
@@ -5119,7 +5432,7 @@ async function enqueueForwarding({ activity, activityId, activityType, baseUrl,
|
|
|
5119
5432
|
type: "outbox",
|
|
5120
5433
|
id: crypto.randomUUID(),
|
|
5121
5434
|
baseUrl,
|
|
5122
|
-
keys
|
|
5435
|
+
keys,
|
|
5123
5436
|
activity,
|
|
5124
5437
|
activityId: activityId.href,
|
|
5125
5438
|
activityType,
|
|
@@ -5145,34 +5458,50 @@ async function enqueueForwarding({ activity, activityId, activityType, baseUrl,
|
|
|
5145
5458
|
}
|
|
5146
5459
|
//#endregion
|
|
5147
5460
|
//#region src/federation/outgoing-proof.ts
|
|
5148
|
-
function isPortableUrl(url) {
|
|
5149
|
-
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
5150
|
-
}
|
|
5151
5461
|
function formatKeyIds(candidates) {
|
|
5152
5462
|
return candidates.map((c) => c.verificationMethod.href).join(", ");
|
|
5153
5463
|
}
|
|
5154
5464
|
/**
|
|
5155
5465
|
* Rejects an activity of a portable actor whose ID cannot be authenticated by
|
|
5156
5466
|
* the actor's DID: FEP-ef61 requires the activity to be a portable object
|
|
5157
|
-
* signed by that DID, so its ID has to be a portable ID
|
|
5467
|
+
* signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
|
|
5468
|
+
* `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
|
|
5469
|
+
* actor is one whose ID is such an ID as well.
|
|
5158
5470
|
* @throws {TypeError} If the activity is performed by a portable actor, but
|
|
5159
|
-
* its ID is not a portable ID of the actor's DID,
|
|
5160
|
-
* portable actors do not share a DID
|
|
5471
|
+
* its ID is not a portable ID of the actor's DID, its
|
|
5472
|
+
* portable actors do not share a DID, or the ID of one of
|
|
5473
|
+
* them is a malformed compatible identifier.
|
|
5161
5474
|
* @internal
|
|
5162
5475
|
*/
|
|
5163
5476
|
function assertPortableActorActivity(activity) {
|
|
5164
|
-
const portableActors = activity.actorIds.filter(
|
|
5477
|
+
const portableActors = activity.actorIds.filter((id) => isPortableId(id));
|
|
5165
5478
|
if (portableActors.length < 1) return;
|
|
5166
|
-
const
|
|
5167
|
-
|
|
5168
|
-
|
|
5479
|
+
const dids = portableActors.map((actorId) => {
|
|
5480
|
+
const did = getPortableDid(actorId);
|
|
5481
|
+
if (did == null) throw new TypeError(`The activity ${activity.id?.href} is performed by the actor ${actorId.href}, whose ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
|
|
5482
|
+
return did;
|
|
5483
|
+
});
|
|
5484
|
+
const did = dids[0];
|
|
5485
|
+
if (dids.some((d) => d !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
|
|
5486
|
+
if (activity.id == null || !isPortableId(activity.id) || getPortableDid(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI, or an FEP-ef61 compatible identifier, with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
|
|
5487
|
+
if (!isCompatibleEf61Iri(activity.id)) return;
|
|
5488
|
+
const mismatched = portableActors.find((actorId) => isCompatibleEf61Iri(actorId) && actorId.origin !== activity.id?.origin);
|
|
5489
|
+
if (mismatched == null) return;
|
|
5490
|
+
getLogger([
|
|
5491
|
+
"fedify",
|
|
5492
|
+
"federation",
|
|
5493
|
+
"outbox"
|
|
5494
|
+
]).warn("The activity {activityId} and its actor {actorId} are identified by FEP-ef61 compatible identifiers on different gateways. FEP-ef61 requires publishers to use the first gateway in the actor's gateways when constructing compatible identifiers.", {
|
|
5495
|
+
activityId: activity.id.href,
|
|
5496
|
+
actorId: mismatched.href
|
|
5497
|
+
});
|
|
5169
5498
|
}
|
|
5170
5499
|
/**
|
|
5171
5500
|
* Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
|
|
5172
5501
|
*
|
|
5173
5502
|
* Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
|
|
5174
5503
|
* never sign proofs. A portable activity, i.e., one with an `ap:` or
|
|
5175
|
-
* `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
|
|
5504
|
+
* `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
|
|
5176
5505
|
* whose verification method is a DID URL for its DID, even if it is the only
|
|
5177
5506
|
* key; if no key or more than one key qualifies, this function throws.
|
|
5178
5507
|
*
|
|
@@ -5211,7 +5540,7 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
5211
5540
|
break;
|
|
5212
5541
|
}
|
|
5213
5542
|
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !isCompatibleKeyId(c.verificationMethod));
|
|
5214
|
-
if (!hasProof && activity.id != null &&
|
|
5543
|
+
if (!hasProof && activity.id != null && isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
|
|
5215
5544
|
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
5216
5545
|
activity,
|
|
5217
5546
|
hasProof,
|
|
@@ -5244,8 +5573,9 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
5244
5573
|
* @throws {TypeError} If no key or more than one key qualifies.
|
|
5245
5574
|
*/
|
|
5246
5575
|
function selectPortableActivityKey(activity, activityId, keys) {
|
|
5247
|
-
const activityDid =
|
|
5248
|
-
|
|
5576
|
+
const activityDid = getPortableDid(activityId);
|
|
5577
|
+
if (activityDid == null) throw new TypeError(`Cannot sign the portable activity ${activityId.href}, as its ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
|
|
5578
|
+
const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && getPortableDid(c.verificationMethod) === activityDid);
|
|
5249
5579
|
if (eligible.length !== 1) throw new TypeError(`Cannot choose an Object Integrity Proof key for the portable activity ${activityId.href}: ` + (eligible.length < 1 ? `none of its ${keys.length} Ed25519 keys (${formatKeyIds(keys)}) ` : `${eligible.length} of its Ed25519 keys (${formatKeyIds(eligible)}) `) + `${eligible.length < 1 ? "is" : "are"} a DID URL verification method for ${activityDid}. FEP-ef61 requires a portable activity's proof to be made by its DID, and the map-local compound-proof profile accepts exactly one direct proof per map. Gateway keys, whose IDs are compatible identifiers, never sign proofs. Supply exactly one Ed25519 key whose key ID is a DID URL for ${activityDid}, or sign the activity with signObject() before sending it.`);
|
|
5250
5580
|
getLogger([
|
|
5251
5581
|
"fedify",
|
|
@@ -5663,6 +5993,27 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
|
|
|
5663
5993
|
};
|
|
5664
5994
|
}
|
|
5665
5995
|
/**
|
|
5996
|
+
* Gets the compatible identifier that a WebFinger response links to for
|
|
5997
|
+
* an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
|
|
5998
|
+
* URI is linked through its compatible identifier on the given gateway, while
|
|
5999
|
+
* an actor identified by a compatible identifier is linked through its ID as
|
|
6000
|
+
* is, so that software without FEP-ef61 support finds the same ID in the actor
|
|
6001
|
+
* document.
|
|
6002
|
+
* @param actorId The portable actor's ID.
|
|
6003
|
+
* @param gateway The actor's first gateway.
|
|
6004
|
+
* @returns The compatible identifier, or `null` if the actor ID is malformed
|
|
6005
|
+
* or cannot be represented as a compatible identifier.
|
|
6006
|
+
*/
|
|
6007
|
+
function getCompatibleActorId(actorId, gateway) {
|
|
6008
|
+
try {
|
|
6009
|
+
if (isPortableUri(actorId)) return toCompatibleEf61Id(actorId, gateway);
|
|
6010
|
+
return fromCompatibleEf61Id(actorId) == null ? null : actorId;
|
|
6011
|
+
} catch (error) {
|
|
6012
|
+
if (error instanceof TypeError) return null;
|
|
6013
|
+
throw error;
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
/**
|
|
5666
6017
|
* Computes the subject and aliases for an FEP-ef61 portable actor, whose
|
|
5667
6018
|
* WebFinger address takes its domain from the first gateway of the actor
|
|
5668
6019
|
* rather than from the actor ID. The canonical `ap:` ID is not emitted,
|
|
@@ -5826,21 +6177,23 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
5826
6177
|
});
|
|
5827
6178
|
let actorUri = context.getActorUri(identifier);
|
|
5828
6179
|
let portable;
|
|
5829
|
-
if (actor.id != null && (actor.id
|
|
6180
|
+
if (actor.id != null && isPortableId(actor.id)) {
|
|
5830
6181
|
const gateway = actor.gateway;
|
|
5831
|
-
|
|
5832
|
-
if (gateway != null && isGatewayUrl(gateway)) try {
|
|
5833
|
-
compatibleId = toCompatibleEf61Id(actor.id, gateway);
|
|
5834
|
-
} catch (error) {
|
|
5835
|
-
if (!(error instanceof TypeError)) throw error;
|
|
5836
|
-
}
|
|
5837
|
-
if (gateway == null || compatibleId == null) {
|
|
6182
|
+
if (gateway == null || !isGatewayUrl(gateway)) {
|
|
5838
6183
|
logger.error("The portable actor {actorId} (identifier: {identifier}) needs an HTTP(S) origin as the first item of its gateways property to be discovered through WebFinger.", {
|
|
5839
6184
|
actorId: formatIri(actor.id),
|
|
5840
6185
|
identifier
|
|
5841
6186
|
});
|
|
5842
6187
|
return await onNotFound(request);
|
|
5843
6188
|
}
|
|
6189
|
+
const compatibleId = getCompatibleActorId(actor.id, gateway);
|
|
6190
|
+
if (compatibleId == null) {
|
|
6191
|
+
logger.error("The portable actor {actorId} (identifier: {identifier}) cannot be discovered through WebFinger, as its ID is malformed or cannot be represented as an FEP-ef61 compatible identifier.", {
|
|
6192
|
+
actorId: formatIri(actor.id),
|
|
6193
|
+
identifier
|
|
6194
|
+
});
|
|
6195
|
+
return await onNotFound(request);
|
|
6196
|
+
}
|
|
5844
6197
|
actorUri = compatibleId;
|
|
5845
6198
|
portable = {
|
|
5846
6199
|
compatibleId,
|
|
@@ -7799,6 +8152,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7799
8152
|
outboxQueue: this.outboxQueue,
|
|
7800
8153
|
startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
|
|
7801
8154
|
allowPrivateAddress: this.allowPrivateAddress,
|
|
8155
|
+
getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
|
|
8156
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
7802
8157
|
options: this.portableInboxForwarding,
|
|
7803
8158
|
meterProvider: this.meterProvider,
|
|
7804
8159
|
tracerProvider: this.tracerProvider
|
|
@@ -7807,6 +8162,49 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7807
8162
|
}
|
|
7808
8163
|
});
|
|
7809
8164
|
}
|
|
8165
|
+
/**
|
|
8166
|
+
* Gets this server's gateway key pairs for the portable actor that owns
|
|
8167
|
+
* a portable inbox, which sign the activities forwarded from the inbox.
|
|
8168
|
+
* @param context The context.
|
|
8169
|
+
* @param identifier The identifier of the portable actor.
|
|
8170
|
+
* @param recipient The portable inbox.
|
|
8171
|
+
* @returns The key pairs, or an empty array if the actor has no gateway
|
|
8172
|
+
* keys on this server.
|
|
8173
|
+
*/
|
|
8174
|
+
async #getPortableGatewayKeyPairs(context, identifier, recipient) {
|
|
8175
|
+
const logger = getLogger([
|
|
8176
|
+
"fedify",
|
|
8177
|
+
"federation",
|
|
8178
|
+
"inbox"
|
|
8179
|
+
]);
|
|
8180
|
+
if (this.actorCallbacks?.keyPairsDispatcher == null || this.actorCallbacks.portableActorIdMapper == null) {
|
|
8181
|
+
logger.debug("Forwarding activities from the portable inbox {inbox} without HTTP Signatures, as gateway keys need both a key pairs dispatcher and a portable actor ID mapper.", { inbox: recipient.canonicalInboxId });
|
|
8182
|
+
return [];
|
|
8183
|
+
}
|
|
8184
|
+
const actorId = getCanonicalPortableId(recipient.actorId);
|
|
8185
|
+
if (actorId == null) return [];
|
|
8186
|
+
const keyPairs = await context.getActorKeyPairs(identifier);
|
|
8187
|
+
for (const { cryptographicKey } of keyPairs) {
|
|
8188
|
+
const owner = cryptographicKey.ownerId;
|
|
8189
|
+
const canonicalOwner = owner == null ? null : getCanonicalPortableId(owner);
|
|
8190
|
+
if (canonicalOwner === actorId) continue;
|
|
8191
|
+
if (canonicalOwner == null) logger.debug("Forwarding activities from the portable inbox {inbox} without HTTP Signatures, as the key pairs of the actor {identifier} are not the gateway keys of a portable actor.", {
|
|
8192
|
+
inbox: recipient.canonicalInboxId,
|
|
8193
|
+
identifier
|
|
8194
|
+
});
|
|
8195
|
+
else logger.warn("Forwarding activities from the portable inbox {inbox} without HTTP Signatures, as the key pairs of the actor {identifier} are the gateway keys of {owner}, not of the recipient {actorId}.", {
|
|
8196
|
+
inbox: recipient.canonicalInboxId,
|
|
8197
|
+
identifier,
|
|
8198
|
+
owner: canonicalOwner,
|
|
8199
|
+
actorId
|
|
8200
|
+
});
|
|
8201
|
+
return [];
|
|
8202
|
+
}
|
|
8203
|
+
return keyPairs.map(({ keyId, privateKey }) => ({
|
|
8204
|
+
keyId,
|
|
8205
|
+
privateKey
|
|
8206
|
+
}));
|
|
8207
|
+
}
|
|
7810
8208
|
async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
|
|
7811
8209
|
metricState.endpoint = "hashlink_media";
|
|
7812
8210
|
metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
|