@fedify/fedify 2.4.0-dev.2159 → 2.4.0-dev.2169
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-D89oOfHC.mjs → builder-B2d_Pt0j.mjs} +2 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-B02Os8h2.mjs → compound-proof-D28gMFqP.mjs} +2 -2
- package/dist/{context-JDNkUtnu.d.ts → context-BykNUf04.d.ts} +1 -1
- package/dist/{context-DpsaHkk1.d.cts → context-L0Fnib9j.d.cts} +1 -1
- package/dist/{deno-CE_s0yQb.mjs → deno-DU8dvnWk.mjs} +1 -1
- package/dist/{docloader-C5p26tpa.mjs → docloader-CghLH7Zi.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/keycache.test.mjs +67 -1
- 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.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +5 -5
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-gateway-keys.test.mjs +180 -6
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable.test.mjs +3 -3
- 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 +1 -1
- package/dist/{federation-dBOqmJcA.cjs → federation-DrNgkLfJ.cjs} +48 -24
- package/dist/{http-wdsia0-O.cjs → http-5CJBdU8n.cjs} +392 -65
- package/dist/{http-nTZuTacw.js → http-BGpexPO7.js} +368 -61
- package/dist/{http-DV_yW4dv.mjs → http-Bd6rZlJX.mjs} +7 -5
- package/dist/{http-C-x_OLSp.d.ts → http-Cok5-vIC.d.ts} +5 -0
- package/dist/{http-CjezPjFX.d.cts → http-DC1cqAQ2.d.cts} +5 -0
- package/dist/{key-C6jnekFI.mjs → key-gIhVZO3p.mjs} +58 -7
- package/dist/key-owner-evidence-Bo3yaeL5.mjs +108 -0
- package/dist/{keycache-BInaECGj.mjs → keycache-PiOM9pM0.mjs} +46 -21
- package/dist/{kv-cache-Bxm0a0Ww.mjs → kv-cache-D0qjSeWY.mjs} +1 -1
- package/dist/{kv-cache-BHNtQi9e.cjs → kv-cache-YRi_g8GB.cjs} +1 -1
- package/dist/{kv-cache-DhIl9YlV.js → kv-cache-iR55fqlk.js} +1 -1
- package/dist/{ld-ya2C1Xip.mjs → ld-BsTkRrwF.mjs} +3 -3
- package/dist/{metrics-ByOQOHHS.mjs → metrics-jUDFfVgi.mjs} +1 -1
- package/dist/{middleware-Bdwd_Gg8.mjs → middleware-Coqgp-K0.mjs} +15 -15
- package/dist/{middleware-WiaN4RS3.js → middleware-DlxMxVp5.js} +48 -24
- package/dist/{middleware-DoVWq8mE.mjs → middleware-Dq-OCmb6.mjs} +1 -1
- package/dist/{mod-Bz9tFXI2.d.ts → mod--jv6vyrP.d.ts} +1 -1
- package/dist/{mod-DaiBrWDW.d.ts → mod-B5z_q9TH.d.ts} +1 -1
- package/dist/{mod-Mt4aO3DX.d.cts → mod-BA_F2x53.d.cts} +1 -1
- package/dist/{mod-DSKkTwH2.d.cts → mod-BLOXGs6m.d.cts} +1 -1
- package/dist/{mod-S--JxYjo.d.cts → mod-CPPqkyV_.d.cts} +1 -1
- package/dist/{mod-_4K05HZF.d.ts → mod-DK0y9fd-.d.ts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +5 -5
- package/dist/mod.d.ts +5 -5
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-C6Nv5yyw.mjs → owner-D3GoetXS.mjs} +18 -3
- package/dist/{portable-collection-B-mU1F1-.js → portable-collection-Bkdh_WpL.js} +15 -1
- package/dist/{portable-collection-uWj3hfzv.mjs → portable-collection-C60_acmx.mjs} +2 -2
- package/dist/{portable-collection-C65SwL2l.cjs → portable-collection-CnlCamup.cjs} +15 -1
- package/dist/{portable-key-BXsVLSMa.mjs → portable-key-DyF5AnZo.mjs} +209 -55
- package/dist/{proof-BdO3BbLh.mjs → proof-CQAYqWdj.mjs} +4 -4
- package/dist/{send-BwANxryy.mjs → send-DjkQrxt5.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 +2 -2
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-key.test.mjs +253 -7
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/{temporal-CbPfRGZq.mjs → temporal-oRKP803M.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.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
|
@@ -14,7 +14,7 @@ import serialize from "json-canon";
|
|
|
14
14
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
15
15
|
//#region deno.json
|
|
16
16
|
var name = "@fedify/fedify";
|
|
17
|
-
var version = "2.4.0-dev.
|
|
17
|
+
var version = "2.4.0-dev.2169+0a45df07";
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/sig/portable-key-id.ts
|
|
20
20
|
const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
|
|
@@ -1181,6 +1181,108 @@ function getDurationMs(start) {
|
|
|
1181
1181
|
return Math.max(0, performance.now() - start);
|
|
1182
1182
|
}
|
|
1183
1183
|
//#endregion
|
|
1184
|
+
//#region src/sig/key-owner-evidence.ts
|
|
1185
|
+
/**
|
|
1186
|
+
* How long a key of an [FEP-ef61] portable actor is trusted after its actor's
|
|
1187
|
+
* signed document vouched for it, however long the proof of the document is
|
|
1188
|
+
* valid. The actor can drop the key or the gateway from its document at any
|
|
1189
|
+
* time, so the key is looked up again at least this often.
|
|
1190
|
+
*
|
|
1191
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
1192
|
+
* @internal
|
|
1193
|
+
*/
|
|
1194
|
+
const MAX_PORTABLE_KEY_TTL = Temporal.Duration.from({ hours: 1 });
|
|
1195
|
+
const candidates = /* @__PURE__ */ new WeakMap();
|
|
1196
|
+
const verified = /* @__PURE__ */ new WeakMap();
|
|
1197
|
+
/**
|
|
1198
|
+
* Attaches to a key that was just resolved the evidence of the portable actor
|
|
1199
|
+
* that owns it. The evidence is not used until the key verifies
|
|
1200
|
+
* a signature; see {@link promoteKeyOwnerEvidence}.
|
|
1201
|
+
*
|
|
1202
|
+
* Nothing is attached unless the document, the key's owner, and the actor
|
|
1203
|
+
* the key ID belongs to agree on the actor's canonical portable ID.
|
|
1204
|
+
* @param key The key, whose owner the document vouched for.
|
|
1205
|
+
* @param document The expanded root node of the actor's verified document.
|
|
1206
|
+
* @param expires When the evidence must not be used anymore.
|
|
1207
|
+
* @returns Whether the evidence was attached.
|
|
1208
|
+
* @internal
|
|
1209
|
+
*/
|
|
1210
|
+
function attachKeyOwnerEvidence(key, document, expires) {
|
|
1211
|
+
if (!(key instanceof CryptographicKey) || key.id == null || key.ownerId == null || key.publicKey == null || document == null || typeof document !== "object" || Array.isArray(document)) return false;
|
|
1212
|
+
const rawId = document["@id"];
|
|
1213
|
+
if (typeof rawId !== "string") return false;
|
|
1214
|
+
const keyBase = new URL(key.id.href);
|
|
1215
|
+
keyBase.hash = "";
|
|
1216
|
+
const ownerId = getCanonicalPortableId(key.ownerId);
|
|
1217
|
+
if (ownerId == null || getRawCanonicalPortableId(rawId) !== ownerId || getCanonicalPortableId(keyBase) !== ownerId) return false;
|
|
1218
|
+
candidates.set(key, {
|
|
1219
|
+
keyId: key.id.href,
|
|
1220
|
+
keyOwner: key.ownerId.href,
|
|
1221
|
+
publicKey: key.publicKey,
|
|
1222
|
+
ownerId,
|
|
1223
|
+
document: structuredClone(document),
|
|
1224
|
+
expires
|
|
1225
|
+
});
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Makes the evidence attached to a key usable, as the key has just verified
|
|
1230
|
+
* an HTTP Signature. Nothing happens if the key has no evidence.
|
|
1231
|
+
* @param key The key that verified a signature.
|
|
1232
|
+
* @internal
|
|
1233
|
+
*/
|
|
1234
|
+
function promoteKeyOwnerEvidence(key) {
|
|
1235
|
+
const evidence = candidates.get(key);
|
|
1236
|
+
if (evidence == null) return;
|
|
1237
|
+
candidates.delete(key);
|
|
1238
|
+
verified.set(key, evidence);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Gets the evidence of the portable actor that owns a key HTTP Signature
|
|
1242
|
+
* verification returned.
|
|
1243
|
+
* @param key The key.
|
|
1244
|
+
* @param now The current time.
|
|
1245
|
+
* @returns The evidence, or `undefined` if the key was not returned by HTTP
|
|
1246
|
+
* Signature verification, if it has no evidence, if the evidence
|
|
1247
|
+
* expired, or if the key has been changed since.
|
|
1248
|
+
* @internal
|
|
1249
|
+
*/
|
|
1250
|
+
function getVerifiedKeyOwnerEvidence(key, now = Temporal.Now.instant()) {
|
|
1251
|
+
const evidence = verified.get(key);
|
|
1252
|
+
if (evidence == null) return void 0;
|
|
1253
|
+
if (Temporal.Instant.compare(now, evidence.expires) >= 0) {
|
|
1254
|
+
verified.delete(key);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (key.id?.href !== evidence.keyId || key.ownerId?.href !== evidence.keyOwner || key.publicKey !== evidence.publicKey) return;
|
|
1258
|
+
return evidence;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Parses the actor from the evidence, i.e., from the expansion of its
|
|
1262
|
+
* document whose proof was verified. Every call returns a new object.
|
|
1263
|
+
* @param evidence The evidence.
|
|
1264
|
+
* @param options Options for parsing the document.
|
|
1265
|
+
* @returns The actor, or `null` if the document is not the actor that
|
|
1266
|
+
* the evidence is about, or the evidence expired meanwhile.
|
|
1267
|
+
* @internal
|
|
1268
|
+
*/
|
|
1269
|
+
async function parseKeyOwnerEvidence(evidence, options = {}) {
|
|
1270
|
+
const refuse = (url) => Promise.reject(/* @__PURE__ */ new Error(`Refusing to fetch ${url}.`));
|
|
1271
|
+
let actor;
|
|
1272
|
+
try {
|
|
1273
|
+
actor = await Object$1.fromJsonLd(structuredClone(evidence.document), {
|
|
1274
|
+
documentLoader: refuse,
|
|
1275
|
+
contextLoader: options.contextLoader,
|
|
1276
|
+
tracerProvider: options.tracerProvider
|
|
1277
|
+
});
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
if (!(error instanceof TypeError)) throw error;
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
if (!isPortableActorDocument(actor) || getCanonicalPortableId(actor.id) !== evidence.ownerId || Temporal.Instant.compare(Temporal.Now.instant(), evidence.expires) >= 0) return null;
|
|
1283
|
+
return actor;
|
|
1284
|
+
}
|
|
1285
|
+
//#endregion
|
|
1184
1286
|
//#region src/sig/key.ts
|
|
1185
1287
|
/**
|
|
1186
1288
|
* Checks if the given key is valid and supported. No-op if the key is valid,
|
|
@@ -1403,6 +1505,7 @@ function bypassKeyCacheReads(keyCache) {
|
|
|
1403
1505
|
const scoped = cache.compatibleKeyScope(scope);
|
|
1404
1506
|
return {
|
|
1405
1507
|
get: () => Promise.resolve(void 0),
|
|
1508
|
+
getEntry: () => Promise.resolve(void 0),
|
|
1406
1509
|
set: async (keyId, key, options) => await scoped.set(keyId, key, options),
|
|
1407
1510
|
delete: async (keyId) => await scoped.delete(keyId)
|
|
1408
1511
|
};
|
|
@@ -1570,6 +1673,49 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
|
|
|
1570
1673
|
}
|
|
1571
1674
|
return null;
|
|
1572
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Looks up a key at a compatible identifier, or at an `ap:` or `ap+ef61:`
|
|
1678
|
+
* URI, in the cache namespace of a purpose, like {@link getCachedFetchKey}.
|
|
1679
|
+
* A key that the verified document of its portable actor was cached with
|
|
1680
|
+
* gets the actor's evidence attached from the same entry; see
|
|
1681
|
+
* {@link attachKeyOwnerEvidence}.
|
|
1682
|
+
*/
|
|
1683
|
+
async function getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger) {
|
|
1684
|
+
if (scopedCache?.getEntry == null) return await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
|
|
1685
|
+
const entry = await scopedCache.getEntry(cacheKey);
|
|
1686
|
+
if (entry === void 0) return null;
|
|
1687
|
+
if (entry === null) {
|
|
1688
|
+
logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId });
|
|
1689
|
+
return {
|
|
1690
|
+
key: null,
|
|
1691
|
+
cached: true
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
const hit = checkCachedKeyHit(entry.key, keyId, cls, logger);
|
|
1695
|
+
if (hit?.key instanceof CryptographicKey && entry.owner != null) attachKeyOwnerEvidence(hit.key, entry.owner, entry.expires);
|
|
1696
|
+
return hit;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Attaches the evidence of the portable actor whose document has just
|
|
1700
|
+
* vouched for a key to the key. The evidence expires with the cache entry
|
|
1701
|
+
* of the key, so that it is never trusted longer than the key is cached.
|
|
1702
|
+
* Without a cache, it expires with the proof of the document, but no later
|
|
1703
|
+
* than {@link MAX_PORTABLE_KEY_TTL}, as a cache entry would.
|
|
1704
|
+
* @param resolution The resolution of the key.
|
|
1705
|
+
* @param scopedCache The cache namespace the key was stored in, if any.
|
|
1706
|
+
* @param cached When the cache entry expires, if the cache stored one.
|
|
1707
|
+
*/
|
|
1708
|
+
function attachResolvedKeyOwnerEvidence(resolution, scopedCache, cached) {
|
|
1709
|
+
let expires;
|
|
1710
|
+
if (scopedCache != null) {
|
|
1711
|
+
if (cached == null) return;
|
|
1712
|
+
expires = cached;
|
|
1713
|
+
} else {
|
|
1714
|
+
expires = Temporal.Now.instant().add(MAX_PORTABLE_KEY_TTL);
|
|
1715
|
+
if (resolution.expires != null && Temporal.Instant.compare(resolution.expires, expires) < 0) expires = resolution.expires;
|
|
1716
|
+
}
|
|
1717
|
+
attachKeyOwnerEvidence(resolution.key, resolution.document, expires);
|
|
1718
|
+
}
|
|
1573
1719
|
async function clearFetchErrorMetadata(keyId, keyCache) {
|
|
1574
1720
|
await keyCache?.setFetchError?.(keyId, null);
|
|
1575
1721
|
}
|
|
@@ -1694,7 +1840,10 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
|
|
|
1694
1840
|
cached: false
|
|
1695
1841
|
};
|
|
1696
1842
|
}
|
|
1697
|
-
await scopedCache?.set(cacheKey, resolution.key, {
|
|
1843
|
+
attachResolvedKeyOwnerEvidence(resolution, scopedCache, await scopedCache?.set(cacheKey, resolution.key, {
|
|
1844
|
+
expires: resolution.expires,
|
|
1845
|
+
owner: resolution.document
|
|
1846
|
+
}));
|
|
1698
1847
|
return {
|
|
1699
1848
|
key: resolution.key,
|
|
1700
1849
|
cached: false
|
|
@@ -1915,7 +2064,7 @@ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, o
|
|
|
1915
2064
|
const keyId = cacheKey.href;
|
|
1916
2065
|
const sharedCache = options.keyCache;
|
|
1917
2066
|
const scopedCache = sharedCache?.compatibleKeyScope?.(getCompatibleKeyScope(cls, options));
|
|
1918
|
-
const cached = await
|
|
2067
|
+
const cached = await getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger);
|
|
1919
2068
|
if (cached != null) return {
|
|
1920
2069
|
result: cached,
|
|
1921
2070
|
outcome: { result: "hit" }
|
|
@@ -1979,7 +2128,7 @@ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable
|
|
|
1979
2128
|
}
|
|
1980
2129
|
const sharedCache = options.keyCache;
|
|
1981
2130
|
const scopedCache = sharedCache?.compatibleKeyScope?.("httpSignature");
|
|
1982
|
-
const cached = await
|
|
2131
|
+
const cached = await getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger);
|
|
1983
2132
|
if (cached != null) return {
|
|
1984
2133
|
result: cached,
|
|
1985
2134
|
outcome: { result: "hit" }
|
|
@@ -1994,7 +2143,10 @@ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable
|
|
|
1994
2143
|
logger.debug("Resolving key {keyId} to verify signature...", { keyId });
|
|
1995
2144
|
const resolution = await resolvers.actorKey(cacheKey);
|
|
1996
2145
|
if (resolution.type === "verified") {
|
|
1997
|
-
await scopedCache?.set(cacheKey, resolution.key, {
|
|
2146
|
+
attachResolvedKeyOwnerEvidence(resolution, scopedCache, await scopedCache?.set(cacheKey, resolution.key, {
|
|
2147
|
+
expires: resolution.expires,
|
|
2148
|
+
owner: resolution.document
|
|
2149
|
+
}));
|
|
1998
2150
|
return {
|
|
1999
2151
|
result: {
|
|
2000
2152
|
key: resolution.key,
|
|
@@ -4334,6 +4486,7 @@ async function verifyPortableKeyDocument(document, actor, keyId, isGatewayKey, o
|
|
|
4334
4486
|
type: "verified",
|
|
4335
4487
|
actor,
|
|
4336
4488
|
key,
|
|
4489
|
+
document: root,
|
|
4337
4490
|
...expires == null ? {} : { expires }
|
|
4338
4491
|
};
|
|
4339
4492
|
}
|
|
@@ -4380,6 +4533,21 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
4380
4533
|
});
|
|
4381
4534
|
}
|
|
4382
4535
|
/**
|
|
4536
|
+
* The maximum number of `@gateway` location hints followed for one `ap:` or
|
|
4537
|
+
* `ap+ef61:` key ID. Whoever made a signature chooses the hints of its key
|
|
4538
|
+
* ID, so fewer of them are followed than when dereferencing objects, which
|
|
4539
|
+
* bounds the gateways one signature can make Fedify ask. Further hints are
|
|
4540
|
+
* ignored.
|
|
4541
|
+
*/
|
|
4542
|
+
const MAX_KEY_ID_GATEWAY_HINTS = 3;
|
|
4543
|
+
/**
|
|
4544
|
+
* The default of {@link PortableActorKeyOptions.lookupTimeout}, in
|
|
4545
|
+
* milliseconds.
|
|
4546
|
+
*/
|
|
4547
|
+
const DEFAULT_KEY_ID_LOOKUP_TIMEOUT = 1e4;
|
|
4548
|
+
/** The longest delay that `setTimeout()` supports, in milliseconds. */
|
|
4549
|
+
const MAX_TIMEOUT = 2 ** 31 - 1;
|
|
4550
|
+
/**
|
|
4383
4551
|
* Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
|
|
4384
4552
|
* `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
|
|
4385
4553
|
* the actor's document, i.e., the key ID without its fragment, is fetched
|
|
@@ -4407,12 +4575,25 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
4407
4575
|
* unreachable gateway would have served, so the result is an `unavailable`
|
|
4408
4576
|
* that is not cacheable.
|
|
4409
4577
|
*
|
|
4578
|
+
* Only the first {@link MAX_KEY_ID_GATEWAY_HINTS} location hints are
|
|
4579
|
+
* followed, and all the gateways share one timeout, see
|
|
4580
|
+
* {@link PortableActorKeyOptions.lookupTimeout}. A gateway that does not
|
|
4581
|
+
* serve the document in time failed to serve it, like an unreachable one,
|
|
4582
|
+
* with an error that has no HTTP status. A gateway whose document could not
|
|
4583
|
+
* be processed in time, and the gateways left untried, make the result an
|
|
4584
|
+
* `unavailable` that is not cacheable. A result that arrives after
|
|
4585
|
+
* the timeout is ignored, even if it vouches for the key.
|
|
4586
|
+
*
|
|
4410
4587
|
* @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
|
|
4411
|
-
* @param options Loaders and
|
|
4588
|
+
* @param options Loaders, telemetry providers, and the timeout.
|
|
4412
4589
|
* @returns The resolution, which is never `legacy`.
|
|
4590
|
+
* @throws {RangeError} If the timeout is not a non-negative number of
|
|
4591
|
+
* milliseconds that `setTimeout()` supports.
|
|
4413
4592
|
* @internal
|
|
4414
4593
|
*/
|
|
4415
4594
|
async function resolvePortableActorKey(keyId, options = {}) {
|
|
4595
|
+
const lookupTimeout = options.lookupTimeout ?? DEFAULT_KEY_ID_LOOKUP_TIMEOUT;
|
|
4596
|
+
if (!Number.isFinite(lookupTimeout) || lookupTimeout < 0 || lookupTimeout > MAX_TIMEOUT) throw new RangeError(`The lookup timeout must be between 0 and ${MAX_TIMEOUT} ms: ${lookupTimeout}`);
|
|
4416
4597
|
const documentLoader = options.documentLoader ?? getDocumentLoader();
|
|
4417
4598
|
const contextLoader = options.contextLoader ?? getDocumentLoader();
|
|
4418
4599
|
let formattedKeyId = keyId.href;
|
|
@@ -4431,75 +4612,67 @@ async function resolvePortableActorKey(keyId, options = {}) {
|
|
|
4431
4612
|
if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
|
|
4432
4613
|
const actorUrl = new URL(keyId.href);
|
|
4433
4614
|
actorUrl.hash = "";
|
|
4615
|
+
const gateways = getPortableGatewayCandidates(actorUrl);
|
|
4616
|
+
if (gateways.length > MAX_KEY_ID_GATEWAY_HINTS) logger.debug("Following only the first {max} location hints of key {keyId}.", {
|
|
4617
|
+
keyId: formattedKeyId,
|
|
4618
|
+
max: MAX_KEY_ID_GATEWAY_HINTS
|
|
4619
|
+
});
|
|
4434
4620
|
const requestUrls = [];
|
|
4435
|
-
for (const gateway of
|
|
4621
|
+
for (const gateway of gateways.slice(0, MAX_KEY_ID_GATEWAY_HINTS)) try {
|
|
4436
4622
|
requestUrls.push(toCompatibleEf61Id(actorUrl, gateway).href);
|
|
4437
4623
|
} catch (error) {
|
|
4438
4624
|
if (!(error instanceof TypeError)) throw error;
|
|
4439
4625
|
return reject(`The key ID cannot be fetched from gateways: ${error}`);
|
|
4440
4626
|
}
|
|
4441
4627
|
if (requestUrls.length < 1) requestUrls.push(formatIri(actorUrl));
|
|
4628
|
+
const controller = new AbortController();
|
|
4629
|
+
const timeoutError = new DOMException(`Timed out looking up the actor of key ${formattedKeyId} after ${lookupTimeout} ms.`, "TimeoutError");
|
|
4630
|
+
let timer;
|
|
4631
|
+
if (lookupTimeout > 0) timer = setTimeout(() => controller.abort(timeoutError), lookupTimeout);
|
|
4632
|
+
else controller.abort(timeoutError);
|
|
4633
|
+
const { signal } = controller;
|
|
4634
|
+
const attemptOptions = {
|
|
4635
|
+
...options,
|
|
4636
|
+
documentLoader: withDeadline(documentLoader, signal),
|
|
4637
|
+
contextLoader: withDeadline(contextLoader, signal),
|
|
4638
|
+
signal,
|
|
4639
|
+
formattedKeyId
|
|
4640
|
+
};
|
|
4442
4641
|
const attempts = [];
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
} catch (error) {
|
|
4448
|
-
logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
|
|
4449
|
-
keyId: formattedKeyId,
|
|
4450
|
-
url,
|
|
4451
|
-
error
|
|
4452
|
-
});
|
|
4453
|
-
attempts.push({
|
|
4454
|
-
type: "retrievalFailed",
|
|
4455
|
-
error
|
|
4456
|
-
});
|
|
4457
|
-
continue;
|
|
4458
|
-
}
|
|
4459
|
-
let resolution;
|
|
4460
|
-
try {
|
|
4461
|
-
const object = await Object$1.fromJsonLd(remoteDocument.document, {
|
|
4462
|
-
documentLoader,
|
|
4463
|
-
contextLoader,
|
|
4464
|
-
tracerProvider: options.tracerProvider
|
|
4465
|
-
});
|
|
4466
|
-
resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
|
|
4467
|
-
...options,
|
|
4468
|
-
documentLoader,
|
|
4469
|
-
contextLoader
|
|
4470
|
-
}) : {
|
|
4471
|
-
type: "rejected",
|
|
4472
|
-
reason: "The document is not a portable actor."
|
|
4473
|
-
};
|
|
4474
|
-
} catch (error) {
|
|
4475
|
-
if (error instanceof TypeError) resolution = {
|
|
4476
|
-
type: "rejected",
|
|
4477
|
-
reason: `The document is malformed: ${error.message}`
|
|
4478
|
-
};
|
|
4479
|
-
else {
|
|
4480
|
-
logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
|
|
4642
|
+
try {
|
|
4643
|
+
for (const url of requestUrls) {
|
|
4644
|
+
if (signal.aborted) {
|
|
4645
|
+
logger.debug("Ran out of time looking up the actor of key {keyId}; gateways from {url} on were not tried.", {
|
|
4481
4646
|
keyId: formattedKeyId,
|
|
4482
|
-
url
|
|
4483
|
-
error
|
|
4647
|
+
url
|
|
4484
4648
|
});
|
|
4485
4649
|
attempts.push({
|
|
4486
4650
|
type: "indeterminate",
|
|
4487
|
-
error
|
|
4651
|
+
error: signal.reason
|
|
4652
|
+
});
|
|
4653
|
+
break;
|
|
4654
|
+
}
|
|
4655
|
+
const state = { processing: false };
|
|
4656
|
+
let attempt;
|
|
4657
|
+
try {
|
|
4658
|
+
attempt = await raceAbort(attemptGateway(url, keyId, attemptOptions, state), signal);
|
|
4659
|
+
} catch (error) {
|
|
4660
|
+
if (!signal.aborted) throw error;
|
|
4661
|
+
logger.debug("Ran out of time looking up the actor of key {keyId} at {url}.", {
|
|
4662
|
+
keyId: formattedKeyId,
|
|
4663
|
+
url
|
|
4664
|
+
});
|
|
4665
|
+
attempts.push({
|
|
4666
|
+
type: state.processing ? "indeterminate" : "retrievalFailed",
|
|
4667
|
+
error: signal.reason
|
|
4488
4668
|
});
|
|
4489
4669
|
continue;
|
|
4490
4670
|
}
|
|
4671
|
+
if (attempt.type === "verified") return attempt;
|
|
4672
|
+
attempts.push(attempt);
|
|
4491
4673
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
|
|
4495
|
-
keyId: formattedKeyId,
|
|
4496
|
-
url,
|
|
4497
|
-
reason
|
|
4498
|
-
});
|
|
4499
|
-
attempts.push({
|
|
4500
|
-
type: "rejected",
|
|
4501
|
-
reason
|
|
4502
|
-
});
|
|
4674
|
+
} finally {
|
|
4675
|
+
clearTimeout(timer);
|
|
4503
4676
|
}
|
|
4504
4677
|
const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
|
|
4505
4678
|
if (rejections.length === attempts.length) return reject(rejections.join(" "));
|
|
@@ -4520,6 +4693,139 @@ async function resolvePortableActorKey(keyId, options = {}) {
|
|
|
4520
4693
|
};
|
|
4521
4694
|
}
|
|
4522
4695
|
/**
|
|
4696
|
+
* Fetches the actor's document of a key at an `ap:` or `ap+ef61:` key ID from
|
|
4697
|
+
* one gateway, and tells whether it vouches for the key. It never rejects;
|
|
4698
|
+
* the caller races it against the lookup's timeout, and ignores what it
|
|
4699
|
+
* resolves to after the timeout.
|
|
4700
|
+
*/
|
|
4701
|
+
async function attemptGateway(url, keyId, options, state) {
|
|
4702
|
+
const { documentLoader, contextLoader, signal, formattedKeyId } = options;
|
|
4703
|
+
let remoteDocument;
|
|
4704
|
+
try {
|
|
4705
|
+
remoteDocument = await documentLoader(url);
|
|
4706
|
+
} catch (error) {
|
|
4707
|
+
logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
|
|
4708
|
+
keyId: formattedKeyId,
|
|
4709
|
+
url,
|
|
4710
|
+
error
|
|
4711
|
+
});
|
|
4712
|
+
return {
|
|
4713
|
+
type: "retrievalFailed",
|
|
4714
|
+
error
|
|
4715
|
+
};
|
|
4716
|
+
}
|
|
4717
|
+
if (signal.aborted) return {
|
|
4718
|
+
type: "indeterminate",
|
|
4719
|
+
error: signal.reason
|
|
4720
|
+
};
|
|
4721
|
+
state.processing = true;
|
|
4722
|
+
let resolution;
|
|
4723
|
+
try {
|
|
4724
|
+
const object = await Object$1.fromJsonLd(remoteDocument.document, {
|
|
4725
|
+
documentLoader,
|
|
4726
|
+
contextLoader,
|
|
4727
|
+
tracerProvider: options.tracerProvider
|
|
4728
|
+
});
|
|
4729
|
+
signal.throwIfAborted();
|
|
4730
|
+
resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, options) : {
|
|
4731
|
+
type: "rejected",
|
|
4732
|
+
reason: "The document is not a portable actor."
|
|
4733
|
+
};
|
|
4734
|
+
} catch (error) {
|
|
4735
|
+
if (error instanceof TypeError && !signal.aborted) resolution = {
|
|
4736
|
+
type: "rejected",
|
|
4737
|
+
reason: `The document is malformed: ${error.message}`
|
|
4738
|
+
};
|
|
4739
|
+
else {
|
|
4740
|
+
logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
|
|
4741
|
+
keyId: formattedKeyId,
|
|
4742
|
+
url,
|
|
4743
|
+
error
|
|
4744
|
+
});
|
|
4745
|
+
return {
|
|
4746
|
+
type: "indeterminate",
|
|
4747
|
+
error
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
if (resolution.type === "verified") return resolution;
|
|
4752
|
+
const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
|
|
4753
|
+
logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
|
|
4754
|
+
keyId: formattedKeyId,
|
|
4755
|
+
url,
|
|
4756
|
+
reason
|
|
4757
|
+
});
|
|
4758
|
+
return {
|
|
4759
|
+
type: "rejected",
|
|
4760
|
+
reason
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4763
|
+
/**
|
|
4764
|
+
* Wraps a document loader so that it gives up when the given signal aborts,
|
|
4765
|
+
* as well as when the signal in its own options does. The wrapped loader is
|
|
4766
|
+
* passed a signal that aborts in either case, but in case it ignores that
|
|
4767
|
+
* signal, what it resolves to after the abort is ignored, and the wrapper
|
|
4768
|
+
* rejects with the reason of the abort at once. After the abort, the wrapped
|
|
4769
|
+
* loader is not called at all.
|
|
4770
|
+
*/
|
|
4771
|
+
function withDeadline(loader, signal) {
|
|
4772
|
+
return (url, options) => {
|
|
4773
|
+
const combined = combineSignals(signal, options?.signal);
|
|
4774
|
+
return raceAbort(combined.signal.aborted ? Promise.reject(combined.signal.reason) : new Promise((resolve) => resolve(loader(url, {
|
|
4775
|
+
...options,
|
|
4776
|
+
signal: combined.signal
|
|
4777
|
+
}))), combined.signal).finally(combined.dispose);
|
|
4778
|
+
};
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4781
|
+
* Makes a signal that aborts when either of the given signals does, with
|
|
4782
|
+
* the reason of the one that aborted first. `AbortSignal.any()` is not
|
|
4783
|
+
* available on every supported runtime, e.g., Bun before 1.1.4.
|
|
4784
|
+
* @returns The signal, and a function that stops listening to the given
|
|
4785
|
+
* signals.
|
|
4786
|
+
*/
|
|
4787
|
+
function combineSignals(signal, other) {
|
|
4788
|
+
if (other == null || other === signal) return {
|
|
4789
|
+
signal,
|
|
4790
|
+
dispose() {}
|
|
4791
|
+
};
|
|
4792
|
+
const controller = new AbortController();
|
|
4793
|
+
const sources = [signal, other];
|
|
4794
|
+
const dispose = () => {
|
|
4795
|
+
for (const source of sources) source.removeEventListener("abort", onAbort);
|
|
4796
|
+
};
|
|
4797
|
+
function onAbort() {
|
|
4798
|
+
dispose();
|
|
4799
|
+
controller.abort(this.reason);
|
|
4800
|
+
}
|
|
4801
|
+
const aborted = sources.find((source) => source.aborted);
|
|
4802
|
+
if (aborted == null) for (const source of sources) source.addEventListener("abort", onAbort);
|
|
4803
|
+
else controller.abort(aborted.reason);
|
|
4804
|
+
return {
|
|
4805
|
+
signal: controller.signal,
|
|
4806
|
+
dispose
|
|
4807
|
+
};
|
|
4808
|
+
}
|
|
4809
|
+
/**
|
|
4810
|
+
* Waits for the given promise unless the given signal aborts first, in which
|
|
4811
|
+
* case it rejects with the reason of the abort. If the signal has aborted by
|
|
4812
|
+
* the time the promise settles, the abort wins as well, so that nothing that
|
|
4813
|
+
* settles after it counts.
|
|
4814
|
+
*/
|
|
4815
|
+
function raceAbort(promise, signal) {
|
|
4816
|
+
return new Promise((resolve, reject) => {
|
|
4817
|
+
const onAbort = () => reject(signal.reason);
|
|
4818
|
+
const settle = (settled) => {
|
|
4819
|
+
signal.removeEventListener("abort", onAbort);
|
|
4820
|
+
if (signal.aborted) onAbort();
|
|
4821
|
+
else settled();
|
|
4822
|
+
};
|
|
4823
|
+
promise.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
4824
|
+
if (signal.aborted) onAbort();
|
|
4825
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4826
|
+
});
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4523
4829
|
* Finds the nodes that stand for the key with the given canonical portable
|
|
4524
4830
|
* ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
|
|
4525
4831
|
* itself, e.g., Mitra lists the keys of portable actors that way while
|
|
@@ -5010,7 +5316,8 @@ async function verifyRequestDetailed(request, options = {}) {
|
|
|
5010
5316
|
if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
|
|
5011
5317
|
else result = await verifyRequestDraft(request, span, metricsContext, options);
|
|
5012
5318
|
recordVerificationResult(span, result);
|
|
5013
|
-
if (
|
|
5319
|
+
if (result.verified) promoteKeyOwnerEvidence(result.key);
|
|
5320
|
+
else span.setStatus({ code: SpanStatusCode.ERROR });
|
|
5014
5321
|
return result;
|
|
5015
5322
|
} catch (error) {
|
|
5016
5323
|
threw = true;
|
|
@@ -5721,4 +6028,4 @@ function timingSafeEqual(a, b) {
|
|
|
5721
6028
|
return result === 0;
|
|
5722
6029
|
}
|
|
5723
6030
|
//#endregion
|
|
5724
|
-
export {
|
|
6031
|
+
export { isAbortError$1 as $, hasSignatureLike as A, fetchKey as B, assertSafeJsonLd as C, isPortableId as Ct, detachSignature as D, name as Dt, createSignature as E, isSamePublicKey as Et, verifyJsonLd as F, verifyKeyOwnership as G, generateCryptoKeyPair as H, verifySignature as I, parseKeyOwnerEvidence as J, MAX_PORTABLE_KEY_TTL as K, wrapContextLoaderForJsonLd as L, isInvalidUrlTypeError as M, signJsonLd as N, getNormalizationContextLoader as O, version as Ot, verifyCompactJsonLd as P, instrumentDocumentLoader as Q, exportJwk as R, InvalidContextReferenceError as S, isCompatibleKeyId as St, compactJsonLd as T, isPortableUri as Tt, importJwk as U, fetchKeyDetailed as V, validateCryptoKey as W, getFederationMetrics as X, getDurationMs as Y, getRemoteHost as Z, verifyPortableObjectProof as _, getCanonicalPortableId as _t, signRequest as a, recordDocumentCache as at, verifyProof as b, getRawCanonicalPortableId as bt, fetchPortableGatewayKey as c, recordOutboxActivity as ct, classifyFep2277CoreType as d, registerQueueDepthGauge as dt, recordCircuitBreakerStateChange as et, createProof as f, preloadedOnlyDocumentLoader as ft, verifyObject as g, validateAcceptSignature as gt, verifyMapLocalProof as h, parseAcceptSignature as ht, selectRequestSignature as i, recordCollectionTotalItems as it, isClearlyMalformedContextReference as j, hasSignature as k, resolvePortableActorKey as l, recordOutboxEnqueue as lt, signObject as m, fulfillAcceptSignature as mt, listRequestSignatures as n, recordCollectionPageItems as nt, verifyRequest as o, recordFanoutRecipients as ot, hasProofLike as p, formatAcceptSignature as pt, getVerifiedKeyOwnerEvidence as q, parseRfc9421SignatureInput as r, recordCollectionRequest as rt, verifyRequestDetailed as s, recordInboxActivity as st, doubleKnock as t, recordCollectionDispatchDuration as tt, InvalidPortableObjectIdError as u, recordWebFingerHandle as ut, verifyPortableObjectProofPolicy as v, getGatewayKeyBase as vt, attachSignature as w, isPortableKeyId as wt, normalizeOutgoingActivityJsonLd as x, hasPortableActor as xt, verifyPortableObjectProofWithRoot as y, getPortableDid as yt, fetchActorDocument as z };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as version, t as name } from "./deno-
|
|
5
|
-
import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-
|
|
4
|
+
import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
|
|
5
|
+
import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-jUDFfVgi.mjs";
|
|
6
6
|
import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
|
|
7
7
|
import { f as parseKeyIdString } from "./portable-key-id-Bg259c4Z.mjs";
|
|
8
|
-
import { a as
|
|
9
|
-
import {
|
|
8
|
+
import { a as promoteKeyOwnerEvidence } from "./key-owner-evidence-Bo3yaeL5.mjs";
|
|
9
|
+
import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-gIhVZO3p.mjs";
|
|
10
|
+
import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-DyF5AnZo.mjs";
|
|
10
11
|
import { getLogger } from "@logtape/logtape";
|
|
11
12
|
import { CryptographicKey } from "@fedify/vocab";
|
|
12
13
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
@@ -474,7 +475,8 @@ async function verifyRequestDetailed(request, options = {}) {
|
|
|
474
475
|
if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
|
|
475
476
|
else result = await verifyRequestDraft(request, span, metricsContext, options);
|
|
476
477
|
recordVerificationResult(span, result);
|
|
477
|
-
if (
|
|
478
|
+
if (result.verified) promoteKeyOwnerEvidence(result.key);
|
|
479
|
+
else span.setStatus({ code: SpanStatusCode.ERROR });
|
|
478
480
|
return result;
|
|
479
481
|
} catch (error) {
|
|
480
482
|
threw = true;
|
|
@@ -24,6 +24,11 @@ type PortableGatewayKeyResolution = {
|
|
|
24
24
|
publicKey: CryptoKey;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
|
+
* The expanded root node of the actor's document whose proof was
|
|
28
|
+
* verified, from which {@link actor} was parsed.
|
|
29
|
+
*/
|
|
30
|
+
readonly document: Record<string, unknown>;
|
|
31
|
+
/**
|
|
27
32
|
* When the proof of the actor's document expires, if it does. The
|
|
28
33
|
* document does not vouch for the key after that.
|
|
29
34
|
*/
|
|
@@ -24,6 +24,11 @@ type PortableGatewayKeyResolution = {
|
|
|
24
24
|
publicKey: CryptoKey;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
|
+
* The expanded root node of the actor's document whose proof was
|
|
28
|
+
* verified, from which {@link actor} was parsed.
|
|
29
|
+
*/
|
|
30
|
+
readonly document: Record<string, unknown>;
|
|
31
|
+
/**
|
|
27
32
|
* When the proof of the actor's document expires, if it does. The
|
|
28
33
|
* document does not vouch for the key after that.
|
|
29
34
|
*/
|