@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
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
2
|
const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
|
|
3
3
|
const require_transformers = require("./transformers-BalCrSsr.cjs");
|
|
4
|
-
const require_http = require("./http-
|
|
4
|
+
const require_http = require("./http-DSaSAN7X.cjs");
|
|
5
5
|
const require_types = require("./types-Be2zbyeG.cjs");
|
|
6
|
-
const require_owner = require("./owner-
|
|
7
|
-
const require_kv_cache = require("./kv-cache-
|
|
6
|
+
const require_owner = require("./owner-szIZl4qI.cjs");
|
|
7
|
+
const require_kv_cache = require("./kv-cache-BSJAlP4C.cjs");
|
|
8
8
|
let _logtape_logtape = require("@logtape/logtape");
|
|
9
9
|
let _fedify_uri_template = require("@fedify/uri-template");
|
|
10
10
|
let _fedify_vocab = require("@fedify/vocab");
|
|
11
11
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
12
|
-
let byte_encodings_hex = require("byte-encodings/hex");
|
|
13
12
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
13
|
+
let _fedify_vocab_runtime_internal_portable_dereference = require("@fedify/vocab-runtime/internal/portable-dereference");
|
|
14
|
+
let byte_encodings_hex = require("byte-encodings/hex");
|
|
14
15
|
let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
|
|
15
16
|
_fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
|
|
16
17
|
let es_toolkit = require("es-toolkit");
|
|
@@ -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 && require_http.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 _fedify_vocab.Tombstone) return actor;
|
|
213
|
+
if (actor.id != null && (0, _fedify_vocab_runtime_internal_portable_dereference.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 = (0, _logtape_logtape.getLogger)([
|
|
745
|
+
"fedify",
|
|
746
|
+
"federation",
|
|
747
|
+
"actor"
|
|
748
|
+
]);
|
|
749
|
+
try {
|
|
750
|
+
(0, _fedify_vocab_runtime.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 || !(0, _fedify_vocab_runtime.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 Temporal$11 = require_http.Temporal;
|
|
@@ -1310,7 +1339,14 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
|
|
|
1310
1339
|
//#endregion
|
|
1311
1340
|
//#region src/sig/compound-proof.ts
|
|
1312
1341
|
const textEncoder = new TextEncoder();
|
|
1313
|
-
|
|
1342
|
+
/**
|
|
1343
|
+
* Checks whether a literal `id` or `@id` value makes a map a portable object:
|
|
1344
|
+
* an `ap:` or `ap+ef61:` URI, or anything that looks like an FEP-ef61
|
|
1345
|
+
* compatible identifier, even a malformed one, which then fails the policy.
|
|
1346
|
+
*/
|
|
1347
|
+
function isPortableIdValue(value) {
|
|
1348
|
+
return typeof value === "string" && require_http.isPortableId(value);
|
|
1349
|
+
}
|
|
1314
1350
|
function isJsonMap(value) {
|
|
1315
1351
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
1316
1352
|
}
|
|
@@ -1556,18 +1592,20 @@ function collectPortableObjects(snapshot) {
|
|
|
1556
1592
|
for (let index = current.value.length - 1; index >= 0; index--) pending.push({
|
|
1557
1593
|
value: current.value[index],
|
|
1558
1594
|
path: childPath(current.path, String(index)),
|
|
1559
|
-
depth: current.depth + 1
|
|
1595
|
+
depth: current.depth + 1,
|
|
1596
|
+
keyOf: current.keyOf
|
|
1560
1597
|
});
|
|
1561
1598
|
continue;
|
|
1562
1599
|
}
|
|
1563
1600
|
if (!isCompoundProofJsonObject(current.value)) continue;
|
|
1564
1601
|
const document = current.value;
|
|
1565
|
-
const id = [document.id, document["@id"]].find(
|
|
1602
|
+
const id = [document.id, document["@id"]].find(isPortableIdValue);
|
|
1566
1603
|
if (typeof id === "string") objects.push(Object.freeze({
|
|
1567
1604
|
path: current.path,
|
|
1568
1605
|
id,
|
|
1569
1606
|
depth: current.depth,
|
|
1570
|
-
document
|
|
1607
|
+
document,
|
|
1608
|
+
...current.keyOf == null ? {} : { keyOf: current.keyOf }
|
|
1571
1609
|
}));
|
|
1572
1610
|
const entries = Object.entries(document).sort(([left], [right]) => comparePaths(left, right));
|
|
1573
1611
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
@@ -1576,7 +1614,11 @@ function collectPortableObjects(snapshot) {
|
|
|
1576
1614
|
pending.push({
|
|
1577
1615
|
value,
|
|
1578
1616
|
path: childPath(current.path, key),
|
|
1579
|
-
depth: current.depth + 1
|
|
1617
|
+
depth: current.depth + 1,
|
|
1618
|
+
...typeof id === "string" && (key === "publicKey" || key === "assertionMethod") ? { keyOf: {
|
|
1619
|
+
path: current.path,
|
|
1620
|
+
member: key
|
|
1621
|
+
} } : {}
|
|
1580
1622
|
});
|
|
1581
1623
|
}
|
|
1582
1624
|
}
|
|
@@ -1584,8 +1626,9 @@ function collectPortableObjects(snapshot) {
|
|
|
1584
1626
|
return objects;
|
|
1585
1627
|
}
|
|
1586
1628
|
/**
|
|
1587
|
-
* Determines whether a received JSON tree contains a portable object ID
|
|
1588
|
-
*
|
|
1629
|
+
* Determines whether a received JSON tree contains a portable object ID, i.e.,
|
|
1630
|
+
* an `ap:` or `ap+ef61:` URI or an FEP-ef61 compatible identifier, outside
|
|
1631
|
+
* a proof or context definition, without exceeding discovery limits.
|
|
1589
1632
|
*
|
|
1590
1633
|
* This is only an applicability check. It does not authenticate the input;
|
|
1591
1634
|
* callers must still use {@link verifyCompoundPortableObjectProofs} to obtain
|
|
@@ -1632,7 +1675,7 @@ function inspectCompoundPortableObjectApplicability(json, limits) {
|
|
|
1632
1675
|
} else if (isJsonMap(value)) {
|
|
1633
1676
|
mapCount++;
|
|
1634
1677
|
if (mapCount > limits.maxMaps) return "indeterminate";
|
|
1635
|
-
if ([value.id, value["@id"]].some(
|
|
1678
|
+
if ([value.id, value["@id"]].some(isPortableIdValue)) return "present";
|
|
1636
1679
|
if (!addByteLength(2)) return "indeterminate";
|
|
1637
1680
|
const children = function* () {
|
|
1638
1681
|
let first = true;
|
|
@@ -1695,7 +1738,8 @@ function findInJsonMaps(json, visit) {
|
|
|
1695
1738
|
/**
|
|
1696
1739
|
* Determines whether an outgoing JSON tree contains portable content that
|
|
1697
1740
|
* requires producer-side proof-shape checks: a map, outside proof and context
|
|
1698
|
-
* values, whose direct `id` or `@id` is a portable ActivityPub URI
|
|
1741
|
+
* values, whose direct `id` or `@id` is a portable ActivityPub URI or an
|
|
1742
|
+
* FEP-ef61 compatible identifier.
|
|
1699
1743
|
*
|
|
1700
1744
|
* This deliberately differs from
|
|
1701
1745
|
* {@link inspectCompoundPortableObjectApplicability}: it applies no resource
|
|
@@ -1708,7 +1752,7 @@ function findInJsonMaps(json, visit) {
|
|
|
1708
1752
|
* @internal
|
|
1709
1753
|
*/
|
|
1710
1754
|
function containsCompoundPortableObject(json) {
|
|
1711
|
-
return findInJsonMaps(json, (map) => [map.id, map["@id"]].some(
|
|
1755
|
+
return findInJsonMaps(json, (map) => [map.id, map["@id"]].some(isPortableIdValue) ? true : void 0) ?? false;
|
|
1712
1756
|
}
|
|
1713
1757
|
/**
|
|
1714
1758
|
* Finds the first direct `proof` member whose value the map-local
|
|
@@ -1805,7 +1849,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
|
|
|
1805
1849
|
if (discovered.status !== "ok") return discovered;
|
|
1806
1850
|
const proofs = await verifyDiscoveredProofDocuments(discovered, options);
|
|
1807
1851
|
const proofByPath = new Map(proofs.map((proof) => [proof.path, proof]));
|
|
1808
|
-
const
|
|
1852
|
+
const verifyPortableObject = async (object) => {
|
|
1809
1853
|
const metadata = {
|
|
1810
1854
|
path: object.path,
|
|
1811
1855
|
id: object.id,
|
|
@@ -1837,16 +1881,174 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
|
|
|
1837
1881
|
reason: { type: "invalidPortableObject" }
|
|
1838
1882
|
});
|
|
1839
1883
|
}
|
|
1884
|
+
};
|
|
1885
|
+
const collected = collectPortableObjects(discovered.snapshot);
|
|
1886
|
+
const objectByPath = new Map(collected.map((object) => [object.path, object]));
|
|
1887
|
+
const isKeyCandidate = (object) => object.keyOf != null && !Object.hasOwn(object.document, "proof");
|
|
1888
|
+
const results = /* @__PURE__ */ new Map();
|
|
1889
|
+
for (const result of await Promise.all(collected.filter((object) => !isKeyCandidate(object)).map(verifyPortableObject))) results.set(result.path, result);
|
|
1890
|
+
let projections = 0;
|
|
1891
|
+
for (const object of collected) {
|
|
1892
|
+
if (!isKeyCandidate(object)) continue;
|
|
1893
|
+
const { path, member } = object.keyOf;
|
|
1894
|
+
const parent = results.get(path);
|
|
1895
|
+
const parentObject = objectByPath.get(path);
|
|
1896
|
+
if (parent?.verified === true && parentObject != null && projections++ < MAX_KEY_PROJECTIONS && await isEmbeddedPortableKey(parentObject.document, member, object.document, parent.id)) continue;
|
|
1897
|
+
results.set(object.path, await verifyPortableObject(object));
|
|
1898
|
+
}
|
|
1899
|
+
const portableObjects = Object.freeze(collected.flatMap((object) => {
|
|
1900
|
+
const result = results.get(object.path);
|
|
1901
|
+
return result == null ? [] : [result];
|
|
1840
1902
|
}));
|
|
1841
1903
|
return {
|
|
1842
1904
|
status: "ok",
|
|
1843
1905
|
verified: portableObjects.length > 0 && proofs.every((proof) => proof.verified) && portableObjects.every((object) => object.verified),
|
|
1844
1906
|
snapshot: discovered.snapshot,
|
|
1845
1907
|
proofs,
|
|
1846
|
-
portableObjects
|
|
1908
|
+
portableObjects,
|
|
1847
1909
|
statistics: discovered.statistics
|
|
1848
1910
|
};
|
|
1849
1911
|
}
|
|
1912
|
+
/**
|
|
1913
|
+
* The maximum number of embedded keys per compound document that are checked
|
|
1914
|
+
* by {@link isEmbeddedPortableKey}, each of which expands a projection of its
|
|
1915
|
+
* parent. Keys beyond it have to carry their own proofs.
|
|
1916
|
+
*/
|
|
1917
|
+
const MAX_KEY_PROJECTIONS = 64;
|
|
1918
|
+
const SECURITY = "https://w3id.org/security#";
|
|
1919
|
+
const KEY_TYPES = /* @__PURE__ */ new Set([`${SECURITY}Key`, `${SECURITY}Multikey`]);
|
|
1920
|
+
const KEY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1921
|
+
"@id",
|
|
1922
|
+
"@type",
|
|
1923
|
+
`${SECURITY}owner`,
|
|
1924
|
+
`${SECURITY}controller`,
|
|
1925
|
+
`${SECURITY}publicKeyPem`,
|
|
1926
|
+
`${SECURITY}publicKeyMultibase`
|
|
1927
|
+
]);
|
|
1928
|
+
/**
|
|
1929
|
+
* Tells whether an unsigned map embedded in a verified portable map is just
|
|
1930
|
+
* a key of the portable object the parent is, e.g., a gateway key of
|
|
1931
|
+
* a portable actor, which the parent's proof covers.
|
|
1932
|
+
*
|
|
1933
|
+
* The key's meaning depends on the contexts active where it sits, so it is
|
|
1934
|
+
* decided on a projection of the parent that keeps only what those contexts
|
|
1935
|
+
* depend on: the parent's `@context`, its types and ID, and the member that
|
|
1936
|
+
* holds the key. That holds only if nothing else in the parent can change
|
|
1937
|
+
* them, so both documents may only use contexts that define no keyword
|
|
1938
|
+
* aliases and no scoped contexts; Fedify's preloaded contexts, the only
|
|
1939
|
+
* remote ones the compound policy loads, alias just `id` and `type`.
|
|
1940
|
+
*
|
|
1941
|
+
* The projection must expand to the parent's verified ID with the key as the
|
|
1942
|
+
* single value of `publicKey` or `assertionMethod`, and the key must be
|
|
1943
|
+
* nothing but a `CryptographicKey` or `Multikey` whose ID is a compatible
|
|
1944
|
+
* identifier of the parent plus a fragment, and whose owner or controller,
|
|
1945
|
+
* if any, is the parent.
|
|
1946
|
+
*/
|
|
1947
|
+
async function isEmbeddedPortableKey(parent, member, key, parentId) {
|
|
1948
|
+
const parentPortableId = getCanonicalPortableIdOf(parentId);
|
|
1949
|
+
if (parentPortableId == null) return false;
|
|
1950
|
+
if (!isSimpleContext(parent["@context"]) || Object.hasOwn(key, "@context") && !isSimpleContext(key["@context"])) return false;
|
|
1951
|
+
const literalIds = [key.id, key["@id"]].filter((id) => id !== void 0);
|
|
1952
|
+
if (literalIds.length < 1 || literalIds.some((id) => id !== literalIds[0]) || typeof literalIds[0] !== "string" || !isKeyIdOf(literalIds[0], parentPortableId)) return false;
|
|
1953
|
+
const projection = {};
|
|
1954
|
+
for (const property of [
|
|
1955
|
+
"@context",
|
|
1956
|
+
"type",
|
|
1957
|
+
"@type",
|
|
1958
|
+
"id",
|
|
1959
|
+
"@id"
|
|
1960
|
+
]) if (Object.hasOwn(parent, property)) projection[property] = parent[property];
|
|
1961
|
+
projection[member] = key;
|
|
1962
|
+
let expanded;
|
|
1963
|
+
try {
|
|
1964
|
+
expanded = await _fedify_vocab_runtime_jsonld.default.expand(structuredClone(projection), {
|
|
1965
|
+
documentLoader: require_http.getNormalizationContextLoader(require_http.preloadedOnlyDocumentLoader),
|
|
1966
|
+
keepFreeFloatingNodes: true
|
|
1967
|
+
});
|
|
1968
|
+
} catch {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
if (expanded.length !== 1 || hasGraphKeyword(expanded)) return false;
|
|
1972
|
+
const node = expanded[0];
|
|
1973
|
+
if (!isJsonMap(node) || typeof node["@id"] !== "string" || getCanonicalPortableIdOf(node["@id"]) !== parentPortableId) return false;
|
|
1974
|
+
const properties = Object.keys(node).filter((property) => property !== "@id" && property !== "@type");
|
|
1975
|
+
if (properties.length !== 1 || properties[0] !== `${SECURITY}${member}`) return false;
|
|
1976
|
+
const values = node[properties[0]];
|
|
1977
|
+
if (!Array.isArray(values) || values.length !== 1) return false;
|
|
1978
|
+
const expandedKey = values[0];
|
|
1979
|
+
if (!isJsonMap(expandedKey) || expandedKey["@id"] !== literalIds[0] || Object.keys(expandedKey).some((property) => !KEY_PROPERTIES.has(property))) return false;
|
|
1980
|
+
const types = expandedKey["@type"];
|
|
1981
|
+
if (types !== void 0 && (!Array.isArray(types) || types.some((type) => typeof type !== "string" || !KEY_TYPES.has(type)))) return false;
|
|
1982
|
+
const pem = expandedKey[`${SECURITY}publicKeyPem`];
|
|
1983
|
+
const multibase = expandedKey[`${SECURITY}publicKeyMultibase`];
|
|
1984
|
+
if (pem === void 0 && multibase === void 0) return false;
|
|
1985
|
+
if (pem !== void 0 && !isSingleStringValue(pem)) return false;
|
|
1986
|
+
if (multibase !== void 0 && !isSingleStringValue(multibase, `${SECURITY}multibase`)) return false;
|
|
1987
|
+
for (const property of [`${SECURITY}owner`, `${SECURITY}controller`]) {
|
|
1988
|
+
const owners = expandedKey[property];
|
|
1989
|
+
if (owners === void 0) continue;
|
|
1990
|
+
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;
|
|
1991
|
+
}
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
function getCanonicalPortableIdOf(id) {
|
|
1995
|
+
try {
|
|
1996
|
+
return require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(id));
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
if (error instanceof TypeError) return null;
|
|
1999
|
+
throw error;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Checks whether an ID is a compatible identifier of the given portable
|
|
2004
|
+
* object plus a fragment.
|
|
2005
|
+
*/
|
|
2006
|
+
function isKeyIdOf(id, portableId) {
|
|
2007
|
+
let url;
|
|
2008
|
+
try {
|
|
2009
|
+
if ((0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id) == null) return false;
|
|
2010
|
+
url = new URL(id);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
if (error instanceof TypeError) return false;
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
if (url.hash.length < 2) return false;
|
|
2016
|
+
url.hash = "";
|
|
2017
|
+
return require_http.getCanonicalPortableId(url) === portableId;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Checks whether a `@context` value defines no keyword aliases and no scoped
|
|
2021
|
+
* contexts: only references to contexts and term definitions that map terms
|
|
2022
|
+
* to IRIs, optionally with an `@id` or `@vocab` type coercion or a datatype.
|
|
2023
|
+
*/
|
|
2024
|
+
function isSimpleContext(context) {
|
|
2025
|
+
if (typeof context === "string") return true;
|
|
2026
|
+
if (Array.isArray(context)) return context.every(isSimpleContext);
|
|
2027
|
+
if (context === void 0 || !isCompoundProofJsonObject(context)) return false;
|
|
2028
|
+
return Object.entries(context).every(([term, definition]) => {
|
|
2029
|
+
if (term.startsWith("@")) return false;
|
|
2030
|
+
if (typeof definition === "string") return !definition.startsWith("@");
|
|
2031
|
+
if (!isCompoundProofJsonObject(definition)) return false;
|
|
2032
|
+
const { "@id": id, "@type": type, ...rest } = definition;
|
|
2033
|
+
return Object.keys(rest).length < 1 && typeof id === "string" && !id.startsWith("@") && (type === void 0 || type === "@id" || type === "@vocab" || typeof type === "string" && !type.startsWith("@"));
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
function hasGraphKeyword(value) {
|
|
2037
|
+
if (Array.isArray(value)) return value.some(hasGraphKeyword);
|
|
2038
|
+
if (!isJsonMap(value)) return false;
|
|
2039
|
+
return Object.entries(value).some(([key, child]) => key === "@graph" || key === "@included" || key === "@reverse" || hasGraphKeyword(child));
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Checks whether expanded values are a single plain string, optionally with
|
|
2043
|
+
* the given datatype.
|
|
2044
|
+
*/
|
|
2045
|
+
function isSingleStringValue(values, datatype) {
|
|
2046
|
+
if (!Array.isArray(values) || values.length !== 1) return false;
|
|
2047
|
+
const value = values[0];
|
|
2048
|
+
if (!isJsonMap(value) || typeof value["@value"] !== "string") return false;
|
|
2049
|
+
const keys = Object.keys(value);
|
|
2050
|
+
return keys.length === 1 || datatype != null && keys.length === 2 && value["@type"] === datatype;
|
|
2051
|
+
}
|
|
1850
2052
|
//#endregion
|
|
1851
2053
|
//#region src/federation/inbox.ts
|
|
1852
2054
|
const Temporal$10 = require_http.Temporal;
|
|
@@ -2569,14 +2771,7 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
2569
2771
|
} });
|
|
2570
2772
|
}
|
|
2571
2773
|
function isRequestedPortableObject(object, canonicalId) {
|
|
2572
|
-
|
|
2573
|
-
if (object.id.protocol !== "ap:" && object.id.protocol !== "ap+ef61:") return false;
|
|
2574
|
-
try {
|
|
2575
|
-
return (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)(object.id)) === canonicalId;
|
|
2576
|
-
} catch (error) {
|
|
2577
|
-
if (error instanceof TypeError) return false;
|
|
2578
|
-
throw error;
|
|
2579
|
-
}
|
|
2774
|
+
return object.id != null && require_http.getCanonicalPortableId(object.id) === canonicalId;
|
|
2580
2775
|
}
|
|
2581
2776
|
async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
2582
2777
|
const expanded = await _fedify_vocab_runtime_jsonld.default.expand(jsonLd, {
|
|
@@ -2588,6 +2783,7 @@ async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
|
2588
2783
|
return typeof id === "string" ? id : null;
|
|
2589
2784
|
}
|
|
2590
2785
|
function isRequestedPortableId(id, canonicalId) {
|
|
2786
|
+
if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && require_http.getCanonicalPortableId(new URL(id)) === canonicalId;
|
|
2591
2787
|
try {
|
|
2592
2788
|
return (0, _fedify_vocab_runtime.canonicalizePortableUri)(id) === canonicalId;
|
|
2593
2789
|
} catch (error) {
|
|
@@ -3450,7 +3646,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3450
3646
|
return await respondInvalidActivity(error);
|
|
3451
3647
|
}
|
|
3452
3648
|
activityVerified = true;
|
|
3453
|
-
if (!skipSignatureVerification &&
|
|
3649
|
+
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
3454
3650
|
proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
|
|
3455
3651
|
contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3456
3652
|
documentLoader: ctx.documentLoader,
|
|
@@ -3634,7 +3830,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3634
3830
|
"http_signatures.verified": httpSigKey != null,
|
|
3635
3831
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
3636
3832
|
});
|
|
3637
|
-
if (!skipSignatureVerification && !proofVerified && (
|
|
3833
|
+
if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableGatewayKey(httpSigKey))) {
|
|
3638
3834
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3639
3835
|
logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
|
|
3640
3836
|
activity: json,
|
|
@@ -3652,6 +3848,32 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3652
3848
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3653
3849
|
});
|
|
3654
3850
|
}
|
|
3851
|
+
if (!skipSignatureVerification && activity.id != null && require_http.isPortableId(activity.id)) {
|
|
3852
|
+
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
3853
|
+
contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3854
|
+
documentLoader: ctx.documentLoader,
|
|
3855
|
+
keyCache,
|
|
3856
|
+
meterProvider,
|
|
3857
|
+
tracerProvider
|
|
3858
|
+
});
|
|
3859
|
+
if (rejection != null) {
|
|
3860
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3861
|
+
logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
|
|
3862
|
+
activity: json,
|
|
3863
|
+
recipient,
|
|
3864
|
+
activityId: activity.id.href,
|
|
3865
|
+
reason: rejection
|
|
3866
|
+
});
|
|
3867
|
+
span.setStatus({
|
|
3868
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
3869
|
+
message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
|
|
3870
|
+
});
|
|
3871
|
+
return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
|
|
3872
|
+
status: 401,
|
|
3873
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3655
3877
|
if (httpSigKey != null && !await require_owner.doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
3656
3878
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3657
3879
|
require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
|
|
@@ -4381,7 +4603,41 @@ const MIN_COMPONENTS = [
|
|
|
4381
4603
|
* establish.
|
|
4382
4604
|
*/
|
|
4383
4605
|
function isPortableGatewayKey(key) {
|
|
4384
|
-
return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.
|
|
4606
|
+
return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableId(key.ownerId);
|
|
4607
|
+
}
|
|
4608
|
+
/**
|
|
4609
|
+
* Checks whether an activity needs an Object Integrity Proof by its DID:
|
|
4610
|
+
* either it is performed by an [FEP-ef61] portable actor, or its own ID is
|
|
4611
|
+
* portable. Both `ap:` URIs and compatible identifiers count as portable.
|
|
4612
|
+
*
|
|
4613
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
4614
|
+
*/
|
|
4615
|
+
function isPortableActivity(activity) {
|
|
4616
|
+
return require_http.hasPortableActor(activity) || activity.id != null && require_http.isPortableId(activity.id);
|
|
4617
|
+
}
|
|
4618
|
+
/**
|
|
4619
|
+
* Verifies that a portable activity's own ID belongs to the DID that signed
|
|
4620
|
+
* it, on the JSON-LD document received. The ID the proof policy checks has
|
|
4621
|
+
* to be the parsed activity's ID, so that a remote context that changes
|
|
4622
|
+
* between loads cannot make the two name different objects.
|
|
4623
|
+
* @returns `null` if the ID is verified, or the reason why it is not.
|
|
4624
|
+
*/
|
|
4625
|
+
async function verifyPortableActivityId(activityId, jsonLd, options) {
|
|
4626
|
+
const expectedId = require_http.getCanonicalPortableId(activityId);
|
|
4627
|
+
if (expectedId == null) return "malformed portable ID";
|
|
4628
|
+
let verification;
|
|
4629
|
+
try {
|
|
4630
|
+
verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
|
|
4631
|
+
} catch (error) {
|
|
4632
|
+
if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
|
|
4633
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
4634
|
+
return "malformed document";
|
|
4635
|
+
}
|
|
4636
|
+
const { result, root } = verification;
|
|
4637
|
+
if (!result.verified) return result.reason.type;
|
|
4638
|
+
const rootId = root?.["@id"];
|
|
4639
|
+
if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
|
|
4640
|
+
return null;
|
|
4385
4641
|
}
|
|
4386
4642
|
//#endregion
|
|
4387
4643
|
//#region src/federation/kv.ts
|
|
@@ -5038,16 +5294,17 @@ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, loc
|
|
|
5038
5294
|
reason: "notFound"
|
|
5039
5295
|
};
|
|
5040
5296
|
const id = actor.id;
|
|
5041
|
-
|
|
5297
|
+
const did = id == null || !require_http.isPortableId(id) ? null : require_http.getPortableDid(id);
|
|
5298
|
+
if (id == null || did == null) return {
|
|
5042
5299
|
status: "rejected",
|
|
5043
5300
|
reason: "notPortable"
|
|
5044
5301
|
};
|
|
5045
|
-
if (
|
|
5302
|
+
if (did !== authority) return {
|
|
5046
5303
|
status: "rejected",
|
|
5047
5304
|
reason: "authorityMismatch"
|
|
5048
5305
|
};
|
|
5049
|
-
const inboxId = actor.inboxId;
|
|
5050
|
-
if (inboxId == null ||
|
|
5306
|
+
const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
|
|
5307
|
+
if (inboxId == null || require_http.getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
|
|
5051
5308
|
status: "rejected",
|
|
5052
5309
|
reason: "inboxMismatch"
|
|
5053
5310
|
};
|
|
@@ -5059,23 +5316,22 @@ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, loc
|
|
|
5059
5316
|
return {
|
|
5060
5317
|
status: "accepted",
|
|
5061
5318
|
recipient: {
|
|
5319
|
+
actorId: id,
|
|
5062
5320
|
inboxId,
|
|
5063
5321
|
canonicalInboxId,
|
|
5064
5322
|
gateways
|
|
5065
5323
|
}
|
|
5066
5324
|
};
|
|
5067
5325
|
}
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
}
|
|
5076
|
-
function canonicalize(iri) {
|
|
5326
|
+
/**
|
|
5327
|
+
* Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
|
|
5328
|
+
* identifier, keeping its query.
|
|
5329
|
+
* @returns The portable URI, or `null` if the ID is neither, or is malformed.
|
|
5330
|
+
*/
|
|
5331
|
+
function toPortableUri(id) {
|
|
5332
|
+
if (require_http.isPortableUri(id)) return id;
|
|
5077
5333
|
try {
|
|
5078
|
-
return (0, _fedify_vocab_runtime.
|
|
5334
|
+
return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
|
|
5079
5335
|
} catch (error) {
|
|
5080
5336
|
if (error instanceof TypeError) return null;
|
|
5081
5337
|
throw error;
|
|
@@ -5092,8 +5348,11 @@ function canonicalize(iri) {
|
|
|
5092
5348
|
* Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
|
|
5093
5349
|
* forwarded.
|
|
5094
5350
|
*
|
|
5095
|
-
* Forwarded requests are
|
|
5096
|
-
*
|
|
5351
|
+
* Forwarded requests are signed with HTTP Signatures by the RSA key among
|
|
5352
|
+
* {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
|
|
5353
|
+
* servers requiring HTTP Signatures accept them, and are sent unsigned
|
|
5354
|
+
* otherwise. Either way, the receiving gateways authenticate the activity by
|
|
5355
|
+
* its own proof.
|
|
5097
5356
|
* @param parameters The parameters.
|
|
5098
5357
|
* @returns The target inbox URLs that the activity was handed off to.
|
|
5099
5358
|
*/
|
|
@@ -5106,7 +5365,7 @@ async function forwardPortableInboxActivity(parameters) {
|
|
|
5106
5365
|
const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
|
|
5107
5366
|
const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
|
|
5108
5367
|
if (kv.cas == null || maxTargets < 1) return [];
|
|
5109
|
-
const canonicalActivityId =
|
|
5368
|
+
const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
|
|
5110
5369
|
const targets = getForwardingTargets(recipient, excludedOrigins);
|
|
5111
5370
|
if (targets.length > maxTargets) {
|
|
5112
5371
|
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.", {
|
|
@@ -5165,20 +5424,59 @@ function getForwardingTargets(recipient, excludedOrigins) {
|
|
|
5165
5424
|
}
|
|
5166
5425
|
return targets;
|
|
5167
5426
|
}
|
|
5168
|
-
|
|
5427
|
+
/**
|
|
5428
|
+
* Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
|
|
5429
|
+
* key, as {@link sendActivity} signs requests only with such a key. Since
|
|
5430
|
+
* the targets have already been claimed, the activity is forwarded unsigned
|
|
5431
|
+
* rather than not at all if the key cannot be resolved or used.
|
|
5432
|
+
*/
|
|
5433
|
+
async function resolveForwardingKey({ getKeys, activityId }) {
|
|
5434
|
+
if (getKeys == null) return null;
|
|
5435
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
5436
|
+
"fedify",
|
|
5437
|
+
"federation",
|
|
5438
|
+
"inbox"
|
|
5439
|
+
]);
|
|
5440
|
+
let key;
|
|
5441
|
+
try {
|
|
5442
|
+
key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
|
|
5443
|
+
} catch (error) {
|
|
5444
|
+
logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
5445
|
+
activityId: activityId.href,
|
|
5446
|
+
error
|
|
5447
|
+
});
|
|
5448
|
+
return null;
|
|
5449
|
+
}
|
|
5450
|
+
if (key == null) return null;
|
|
5451
|
+
try {
|
|
5452
|
+
require_http.validateCryptoKey(key.privateKey, "private");
|
|
5453
|
+
} catch (error) {
|
|
5454
|
+
if (!(error instanceof TypeError)) throw error;
|
|
5455
|
+
logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
5456
|
+
keyId: key.keyId.href,
|
|
5457
|
+
activityId: activityId.href,
|
|
5458
|
+
error
|
|
5459
|
+
});
|
|
5460
|
+
return null;
|
|
5461
|
+
}
|
|
5462
|
+
return key;
|
|
5463
|
+
}
|
|
5464
|
+
async function forwardImmediately(parameters, inboxes) {
|
|
5465
|
+
const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
|
|
5169
5466
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
5170
5467
|
"fedify",
|
|
5171
5468
|
"federation",
|
|
5172
5469
|
"inbox"
|
|
5173
5470
|
]);
|
|
5174
5471
|
const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
|
|
5175
|
-
const
|
|
5472
|
+
const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
|
|
5176
5473
|
activity,
|
|
5177
5474
|
activityId: activityId.href,
|
|
5178
5475
|
activityType,
|
|
5179
|
-
keys: [],
|
|
5476
|
+
keys: key == null ? [] : [key],
|
|
5180
5477
|
inbox,
|
|
5181
5478
|
allowPrivateAddress,
|
|
5479
|
+
specDeterminer,
|
|
5182
5480
|
meterProvider,
|
|
5183
5481
|
tracerProvider
|
|
5184
5482
|
}).catch((error) => {
|
|
@@ -5187,9 +5485,9 @@ async function forwardImmediately({ activity, activityId, activityType, allowPri
|
|
|
5187
5485
|
inbox: inbox.href,
|
|
5188
5486
|
error
|
|
5189
5487
|
});
|
|
5190
|
-
}));
|
|
5488
|
+
}))));
|
|
5191
5489
|
let timer;
|
|
5192
|
-
const timedOut = await Promise.race([
|
|
5490
|
+
const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
|
|
5193
5491
|
timer = setTimeout(() => resolve(true), deadline);
|
|
5194
5492
|
})]);
|
|
5195
5493
|
clearTimeout(timer);
|
|
@@ -5198,12 +5496,27 @@ async function forwardImmediately({ activity, activityId, activityType, allowPri
|
|
|
5198
5496
|
deadline
|
|
5199
5497
|
});
|
|
5200
5498
|
}
|
|
5201
|
-
async function enqueueForwarding(
|
|
5499
|
+
async function enqueueForwarding(parameters, outboxQueue, inboxes) {
|
|
5500
|
+
const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
|
|
5202
5501
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
5203
5502
|
"fedify",
|
|
5204
5503
|
"federation",
|
|
5205
5504
|
"inbox"
|
|
5206
5505
|
]);
|
|
5506
|
+
const key = await resolveForwardingKey(parameters);
|
|
5507
|
+
const keys = [];
|
|
5508
|
+
if (key != null) try {
|
|
5509
|
+
keys.push({
|
|
5510
|
+
keyId: key.keyId.href,
|
|
5511
|
+
privateKey: await require_http.exportJwk(key.privateKey)
|
|
5512
|
+
});
|
|
5513
|
+
} catch (error) {
|
|
5514
|
+
logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
5515
|
+
keyId: key.keyId.href,
|
|
5516
|
+
activityId: activityId.href,
|
|
5517
|
+
error
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5207
5520
|
startQueue?.();
|
|
5208
5521
|
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
5209
5522
|
const traceContext = {};
|
|
@@ -5213,7 +5526,7 @@ async function enqueueForwarding({ activity, activityId, activityType, baseUrl,
|
|
|
5213
5526
|
type: "outbox",
|
|
5214
5527
|
id: crypto.randomUUID(),
|
|
5215
5528
|
baseUrl,
|
|
5216
|
-
keys
|
|
5529
|
+
keys,
|
|
5217
5530
|
activity,
|
|
5218
5531
|
activityId: activityId.href,
|
|
5219
5532
|
activityType,
|
|
@@ -5239,34 +5552,50 @@ async function enqueueForwarding({ activity, activityId, activityType, baseUrl,
|
|
|
5239
5552
|
}
|
|
5240
5553
|
//#endregion
|
|
5241
5554
|
//#region src/federation/outgoing-proof.ts
|
|
5242
|
-
function isPortableUrl(url) {
|
|
5243
|
-
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
5244
|
-
}
|
|
5245
5555
|
function formatKeyIds(candidates) {
|
|
5246
5556
|
return candidates.map((c) => c.verificationMethod.href).join(", ");
|
|
5247
5557
|
}
|
|
5248
5558
|
/**
|
|
5249
5559
|
* Rejects an activity of a portable actor whose ID cannot be authenticated by
|
|
5250
5560
|
* the actor's DID: FEP-ef61 requires the activity to be a portable object
|
|
5251
|
-
* signed by that DID, so its ID has to be a portable ID
|
|
5561
|
+
* signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
|
|
5562
|
+
* `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
|
|
5563
|
+
* actor is one whose ID is such an ID as well.
|
|
5252
5564
|
* @throws {TypeError} If the activity is performed by a portable actor, but
|
|
5253
|
-
* its ID is not a portable ID of the actor's DID,
|
|
5254
|
-
* portable actors do not share a DID
|
|
5565
|
+
* its ID is not a portable ID of the actor's DID, its
|
|
5566
|
+
* portable actors do not share a DID, or the ID of one of
|
|
5567
|
+
* them is a malformed compatible identifier.
|
|
5255
5568
|
* @internal
|
|
5256
5569
|
*/
|
|
5257
5570
|
function assertPortableActorActivity(activity) {
|
|
5258
|
-
const portableActors = activity.actorIds.filter(
|
|
5571
|
+
const portableActors = activity.actorIds.filter((id) => require_http.isPortableId(id));
|
|
5259
5572
|
if (portableActors.length < 1) return;
|
|
5260
|
-
const
|
|
5261
|
-
|
|
5262
|
-
|
|
5573
|
+
const dids = portableActors.map((actorId) => {
|
|
5574
|
+
const did = require_http.getPortableDid(actorId);
|
|
5575
|
+
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.`);
|
|
5576
|
+
return did;
|
|
5577
|
+
});
|
|
5578
|
+
const did = dids[0];
|
|
5579
|
+
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.`);
|
|
5580
|
+
if (activity.id == null || !require_http.isPortableId(activity.id) || require_http.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.`);
|
|
5581
|
+
if (!(0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(activity.id)) return;
|
|
5582
|
+
const mismatched = portableActors.find((actorId) => (0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(actorId) && actorId.origin !== activity.id?.origin);
|
|
5583
|
+
if (mismatched == null) return;
|
|
5584
|
+
(0, _logtape_logtape.getLogger)([
|
|
5585
|
+
"fedify",
|
|
5586
|
+
"federation",
|
|
5587
|
+
"outbox"
|
|
5588
|
+
]).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.", {
|
|
5589
|
+
activityId: activity.id.href,
|
|
5590
|
+
actorId: mismatched.href
|
|
5591
|
+
});
|
|
5263
5592
|
}
|
|
5264
5593
|
/**
|
|
5265
5594
|
* Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
|
|
5266
5595
|
*
|
|
5267
5596
|
* Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
|
|
5268
5597
|
* never sign proofs. A portable activity, i.e., one with an `ap:` or
|
|
5269
|
-
* `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
|
|
5598
|
+
* `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
|
|
5270
5599
|
* whose verification method is a DID URL for its DID, even if it is the only
|
|
5271
5600
|
* key; if no key or more than one key qualifies, this function throws.
|
|
5272
5601
|
*
|
|
@@ -5305,7 +5634,7 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
5305
5634
|
break;
|
|
5306
5635
|
}
|
|
5307
5636
|
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !require_http.isCompatibleKeyId(c.verificationMethod));
|
|
5308
|
-
if (!hasProof && activity.id != null &&
|
|
5637
|
+
if (!hasProof && activity.id != null && require_http.isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
|
|
5309
5638
|
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
5310
5639
|
activity,
|
|
5311
5640
|
hasProof,
|
|
@@ -5338,8 +5667,9 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
5338
5667
|
* @throws {TypeError} If no key or more than one key qualifies.
|
|
5339
5668
|
*/
|
|
5340
5669
|
function selectPortableActivityKey(activity, activityId, keys) {
|
|
5341
|
-
const activityDid =
|
|
5342
|
-
|
|
5670
|
+
const activityDid = require_http.getPortableDid(activityId);
|
|
5671
|
+
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.`);
|
|
5672
|
+
const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && require_http.getPortableDid(c.verificationMethod) === activityDid);
|
|
5343
5673
|
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.`);
|
|
5344
5674
|
(0, _logtape_logtape.getLogger)([
|
|
5345
5675
|
"fedify",
|
|
@@ -5759,6 +6089,27 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
|
|
|
5759
6089
|
};
|
|
5760
6090
|
}
|
|
5761
6091
|
/**
|
|
6092
|
+
* Gets the compatible identifier that a WebFinger response links to for
|
|
6093
|
+
* an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
|
|
6094
|
+
* URI is linked through its compatible identifier on the given gateway, while
|
|
6095
|
+
* an actor identified by a compatible identifier is linked through its ID as
|
|
6096
|
+
* is, so that software without FEP-ef61 support finds the same ID in the actor
|
|
6097
|
+
* document.
|
|
6098
|
+
* @param actorId The portable actor's ID.
|
|
6099
|
+
* @param gateway The actor's first gateway.
|
|
6100
|
+
* @returns The compatible identifier, or `null` if the actor ID is malformed
|
|
6101
|
+
* or cannot be represented as a compatible identifier.
|
|
6102
|
+
*/
|
|
6103
|
+
function getCompatibleActorId(actorId, gateway) {
|
|
6104
|
+
try {
|
|
6105
|
+
if (require_http.isPortableUri(actorId)) return (0, _fedify_vocab_runtime.toCompatibleEf61Id)(actorId, gateway);
|
|
6106
|
+
return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(actorId) == null ? null : actorId;
|
|
6107
|
+
} catch (error) {
|
|
6108
|
+
if (error instanceof TypeError) return null;
|
|
6109
|
+
throw error;
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
/**
|
|
5762
6113
|
* Computes the subject and aliases for an FEP-ef61 portable actor, whose
|
|
5763
6114
|
* WebFinger address takes its domain from the first gateway of the actor
|
|
5764
6115
|
* rather than from the actor ID. The canonical `ap:` ID is not emitted,
|
|
@@ -5922,21 +6273,23 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
5922
6273
|
});
|
|
5923
6274
|
let actorUri = context.getActorUri(identifier);
|
|
5924
6275
|
let portable;
|
|
5925
|
-
if (actor.id != null && (actor.id
|
|
6276
|
+
if (actor.id != null && require_http.isPortableId(actor.id)) {
|
|
5926
6277
|
const gateway = actor.gateway;
|
|
5927
|
-
|
|
5928
|
-
if (gateway != null && (0, _fedify_vocab_runtime.isGatewayUrl)(gateway)) try {
|
|
5929
|
-
compatibleId = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(actor.id, gateway);
|
|
5930
|
-
} catch (error) {
|
|
5931
|
-
if (!(error instanceof TypeError)) throw error;
|
|
5932
|
-
}
|
|
5933
|
-
if (gateway == null || compatibleId == null) {
|
|
6278
|
+
if (gateway == null || !(0, _fedify_vocab_runtime.isGatewayUrl)(gateway)) {
|
|
5934
6279
|
logger$1.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.", {
|
|
5935
6280
|
actorId: (0, _fedify_vocab_runtime.formatIri)(actor.id),
|
|
5936
6281
|
identifier
|
|
5937
6282
|
});
|
|
5938
6283
|
return await onNotFound(request);
|
|
5939
6284
|
}
|
|
6285
|
+
const compatibleId = getCompatibleActorId(actor.id, gateway);
|
|
6286
|
+
if (compatibleId == null) {
|
|
6287
|
+
logger$1.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.", {
|
|
6288
|
+
actorId: (0, _fedify_vocab_runtime.formatIri)(actor.id),
|
|
6289
|
+
identifier
|
|
6290
|
+
});
|
|
6291
|
+
return await onNotFound(request);
|
|
6292
|
+
}
|
|
5940
6293
|
actorUri = compatibleId;
|
|
5941
6294
|
portable = {
|
|
5942
6295
|
compatibleId,
|
|
@@ -7896,6 +8249,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7896
8249
|
outboxQueue: this.outboxQueue,
|
|
7897
8250
|
startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
|
|
7898
8251
|
allowPrivateAddress: this.allowPrivateAddress,
|
|
8252
|
+
getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
|
|
8253
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
7899
8254
|
options: this.portableInboxForwarding,
|
|
7900
8255
|
meterProvider: this.meterProvider,
|
|
7901
8256
|
tracerProvider: this.tracerProvider
|
|
@@ -7904,6 +8259,49 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7904
8259
|
}
|
|
7905
8260
|
});
|
|
7906
8261
|
}
|
|
8262
|
+
/**
|
|
8263
|
+
* Gets this server's gateway key pairs for the portable actor that owns
|
|
8264
|
+
* a portable inbox, which sign the activities forwarded from the inbox.
|
|
8265
|
+
* @param context The context.
|
|
8266
|
+
* @param identifier The identifier of the portable actor.
|
|
8267
|
+
* @param recipient The portable inbox.
|
|
8268
|
+
* @returns The key pairs, or an empty array if the actor has no gateway
|
|
8269
|
+
* keys on this server.
|
|
8270
|
+
*/
|
|
8271
|
+
async #getPortableGatewayKeyPairs(context, identifier, recipient) {
|
|
8272
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
8273
|
+
"fedify",
|
|
8274
|
+
"federation",
|
|
8275
|
+
"inbox"
|
|
8276
|
+
]);
|
|
8277
|
+
if (this.actorCallbacks?.keyPairsDispatcher == null || this.actorCallbacks.portableActorIdMapper == null) {
|
|
8278
|
+
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 });
|
|
8279
|
+
return [];
|
|
8280
|
+
}
|
|
8281
|
+
const actorId = require_http.getCanonicalPortableId(recipient.actorId);
|
|
8282
|
+
if (actorId == null) return [];
|
|
8283
|
+
const keyPairs = await context.getActorKeyPairs(identifier);
|
|
8284
|
+
for (const { cryptographicKey } of keyPairs) {
|
|
8285
|
+
const owner = cryptographicKey.ownerId;
|
|
8286
|
+
const canonicalOwner = owner == null ? null : require_http.getCanonicalPortableId(owner);
|
|
8287
|
+
if (canonicalOwner === actorId) continue;
|
|
8288
|
+
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.", {
|
|
8289
|
+
inbox: recipient.canonicalInboxId,
|
|
8290
|
+
identifier
|
|
8291
|
+
});
|
|
8292
|
+
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}.", {
|
|
8293
|
+
inbox: recipient.canonicalInboxId,
|
|
8294
|
+
identifier,
|
|
8295
|
+
owner: canonicalOwner,
|
|
8296
|
+
actorId
|
|
8297
|
+
});
|
|
8298
|
+
return [];
|
|
8299
|
+
}
|
|
8300
|
+
return keyPairs.map(({ keyId, privateKey }) => ({
|
|
8301
|
+
keyId,
|
|
8302
|
+
privateKey
|
|
8303
|
+
}));
|
|
8304
|
+
}
|
|
7907
8305
|
async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
|
|
7908
8306
|
metricState.endpoint = "hashlink_media";
|
|
7909
8307
|
metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
|