@fedify/fedify 2.4.0-dev.2164 → 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-BqRmnx7p.mjs → builder-B2d_Pt0j.mjs} +2 -2
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-CxDBdtNz.mjs → compound-proof-D28gMFqP.mjs} +2 -2
- package/dist/{deno-CizGaYeG.mjs → deno-DU8dvnWk.mjs} +1 -1
- package/dist/{docloader-BBFAItNB.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/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 +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 +4 -4
- 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-DFCnp94y.cjs → federation-DrNgkLfJ.cjs} +3 -3
- package/dist/{http-R4g1McwE.cjs → http-5CJBdU8n.cjs} +208 -55
- package/dist/{http-Baovr6xq.js → http-BGpexPO7.js} +208 -55
- package/dist/{http-DRH0n8TD.mjs → http-Bd6rZlJX.mjs} +4 -4
- package/dist/{key-D5awqp7u.mjs → key-gIhVZO3p.mjs} +2 -2
- package/dist/{kv-cache-C7RWL7hz.mjs → kv-cache-D0qjSeWY.mjs} +1 -1
- package/dist/{kv-cache-Ct-KWRta.cjs → kv-cache-YRi_g8GB.cjs} +1 -1
- package/dist/{kv-cache-CZiBDLPi.js → kv-cache-iR55fqlk.js} +1 -1
- package/dist/{ld-CyliEU48.mjs → ld-BsTkRrwF.mjs} +3 -3
- package/dist/{metrics-CFBEehTc.mjs → metrics-jUDFfVgi.mjs} +1 -1
- package/dist/{middleware-B99JjvFn.mjs → middleware-Coqgp-K0.mjs} +14 -14
- package/dist/{middleware-CdpTFsa2.js → middleware-DlxMxVp5.js} +3 -3
- package/dist/{middleware-BmqEWsWP.mjs → middleware-Dq-OCmb6.mjs} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-DKdtsrEq.mjs → owner-D3GoetXS.mjs} +3 -3
- package/dist/{portable-collection-uv4CuQXm.js → portable-collection-Bkdh_WpL.js} +1 -1
- package/dist/{portable-collection-Cb5cdPIU.mjs → portable-collection-C60_acmx.mjs} +2 -2
- package/dist/{portable-collection-UajqDrSL.cjs → portable-collection-CnlCamup.cjs} +1 -1
- package/dist/{portable-key-Bv1RdQ0n.mjs → portable-key-DyF5AnZo.mjs} +208 -55
- package/dist/{proof-CRTPWbVh.mjs → proof-CQAYqWdj.mjs} +4 -4
- package/dist/{send-2dULlZc8.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.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +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 +252 -6
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/{temporal-Dhsk6aUT.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.js +1 -1
- package/package.json +7 -7
|
@@ -16,7 +16,7 @@ json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
|
|
|
16
16
|
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
17
17
|
//#region deno.json
|
|
18
18
|
var name = "@fedify/fedify";
|
|
19
|
-
var version = "2.4.0-dev.
|
|
19
|
+
var version = "2.4.0-dev.2169+0a45df07";
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/sig/portable-key-id.ts
|
|
22
22
|
const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
|
|
@@ -8102,6 +8102,21 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
8102
8102
|
});
|
|
8103
8103
|
}
|
|
8104
8104
|
/**
|
|
8105
|
+
* The maximum number of `@gateway` location hints followed for one `ap:` or
|
|
8106
|
+
* `ap+ef61:` key ID. Whoever made a signature chooses the hints of its key
|
|
8107
|
+
* ID, so fewer of them are followed than when dereferencing objects, which
|
|
8108
|
+
* bounds the gateways one signature can make Fedify ask. Further hints are
|
|
8109
|
+
* ignored.
|
|
8110
|
+
*/
|
|
8111
|
+
const MAX_KEY_ID_GATEWAY_HINTS = 3;
|
|
8112
|
+
/**
|
|
8113
|
+
* The default of {@link PortableActorKeyOptions.lookupTimeout}, in
|
|
8114
|
+
* milliseconds.
|
|
8115
|
+
*/
|
|
8116
|
+
const DEFAULT_KEY_ID_LOOKUP_TIMEOUT = 1e4;
|
|
8117
|
+
/** The longest delay that `setTimeout()` supports, in milliseconds. */
|
|
8118
|
+
const MAX_TIMEOUT = 2 ** 31 - 1;
|
|
8119
|
+
/**
|
|
8105
8120
|
* Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
|
|
8106
8121
|
* `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
|
|
8107
8122
|
* the actor's document, i.e., the key ID without its fragment, is fetched
|
|
@@ -8129,12 +8144,25 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
8129
8144
|
* unreachable gateway would have served, so the result is an `unavailable`
|
|
8130
8145
|
* that is not cacheable.
|
|
8131
8146
|
*
|
|
8147
|
+
* Only the first {@link MAX_KEY_ID_GATEWAY_HINTS} location hints are
|
|
8148
|
+
* followed, and all the gateways share one timeout, see
|
|
8149
|
+
* {@link PortableActorKeyOptions.lookupTimeout}. A gateway that does not
|
|
8150
|
+
* serve the document in time failed to serve it, like an unreachable one,
|
|
8151
|
+
* with an error that has no HTTP status. A gateway whose document could not
|
|
8152
|
+
* be processed in time, and the gateways left untried, make the result an
|
|
8153
|
+
* `unavailable` that is not cacheable. A result that arrives after
|
|
8154
|
+
* the timeout is ignored, even if it vouches for the key.
|
|
8155
|
+
*
|
|
8132
8156
|
* @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
|
|
8133
|
-
* @param options Loaders and
|
|
8157
|
+
* @param options Loaders, telemetry providers, and the timeout.
|
|
8134
8158
|
* @returns The resolution, which is never `legacy`.
|
|
8159
|
+
* @throws {RangeError} If the timeout is not a non-negative number of
|
|
8160
|
+
* milliseconds that `setTimeout()` supports.
|
|
8135
8161
|
* @internal
|
|
8136
8162
|
*/
|
|
8137
8163
|
async function resolvePortableActorKey(keyId, options = {}) {
|
|
8164
|
+
const lookupTimeout = options.lookupTimeout ?? DEFAULT_KEY_ID_LOOKUP_TIMEOUT;
|
|
8165
|
+
if (!Number.isFinite(lookupTimeout) || lookupTimeout < 0 || lookupTimeout > MAX_TIMEOUT) throw new RangeError(`The lookup timeout must be between 0 and ${MAX_TIMEOUT} ms: ${lookupTimeout}`);
|
|
8138
8166
|
const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
8139
8167
|
const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
8140
8168
|
let formattedKeyId = keyId.href;
|
|
@@ -8153,75 +8181,67 @@ async function resolvePortableActorKey(keyId, options = {}) {
|
|
|
8153
8181
|
if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
|
|
8154
8182
|
const actorUrl = new URL(keyId.href);
|
|
8155
8183
|
actorUrl.hash = "";
|
|
8184
|
+
const gateways = (0, _fedify_vocab_runtime_internal_portable_dereference.getPortableGatewayCandidates)(actorUrl);
|
|
8185
|
+
if (gateways.length > MAX_KEY_ID_GATEWAY_HINTS) logger.debug("Following only the first {max} location hints of key {keyId}.", {
|
|
8186
|
+
keyId: formattedKeyId,
|
|
8187
|
+
max: MAX_KEY_ID_GATEWAY_HINTS
|
|
8188
|
+
});
|
|
8156
8189
|
const requestUrls = [];
|
|
8157
|
-
for (const gateway of (0,
|
|
8190
|
+
for (const gateway of gateways.slice(0, MAX_KEY_ID_GATEWAY_HINTS)) try {
|
|
8158
8191
|
requestUrls.push((0, _fedify_vocab_runtime.toCompatibleEf61Id)(actorUrl, gateway).href);
|
|
8159
8192
|
} catch (error) {
|
|
8160
8193
|
if (!(error instanceof TypeError)) throw error;
|
|
8161
8194
|
return reject(`The key ID cannot be fetched from gateways: ${error}`);
|
|
8162
8195
|
}
|
|
8163
8196
|
if (requestUrls.length < 1) requestUrls.push((0, _fedify_vocab_runtime.formatIri)(actorUrl));
|
|
8197
|
+
const controller = new AbortController();
|
|
8198
|
+
const timeoutError = new DOMException(`Timed out looking up the actor of key ${formattedKeyId} after ${lookupTimeout} ms.`, "TimeoutError");
|
|
8199
|
+
let timer;
|
|
8200
|
+
if (lookupTimeout > 0) timer = setTimeout(() => controller.abort(timeoutError), lookupTimeout);
|
|
8201
|
+
else controller.abort(timeoutError);
|
|
8202
|
+
const { signal } = controller;
|
|
8203
|
+
const attemptOptions = {
|
|
8204
|
+
...options,
|
|
8205
|
+
documentLoader: withDeadline(documentLoader, signal),
|
|
8206
|
+
contextLoader: withDeadline(contextLoader, signal),
|
|
8207
|
+
signal,
|
|
8208
|
+
formattedKeyId
|
|
8209
|
+
};
|
|
8164
8210
|
const attempts = [];
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
} catch (error) {
|
|
8170
|
-
logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
|
|
8171
|
-
keyId: formattedKeyId,
|
|
8172
|
-
url,
|
|
8173
|
-
error
|
|
8174
|
-
});
|
|
8175
|
-
attempts.push({
|
|
8176
|
-
type: "retrievalFailed",
|
|
8177
|
-
error
|
|
8178
|
-
});
|
|
8179
|
-
continue;
|
|
8180
|
-
}
|
|
8181
|
-
let resolution;
|
|
8182
|
-
try {
|
|
8183
|
-
const object = await _fedify_vocab.Object.fromJsonLd(remoteDocument.document, {
|
|
8184
|
-
documentLoader,
|
|
8185
|
-
contextLoader,
|
|
8186
|
-
tracerProvider: options.tracerProvider
|
|
8187
|
-
});
|
|
8188
|
-
resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
|
|
8189
|
-
...options,
|
|
8190
|
-
documentLoader,
|
|
8191
|
-
contextLoader
|
|
8192
|
-
}) : {
|
|
8193
|
-
type: "rejected",
|
|
8194
|
-
reason: "The document is not a portable actor."
|
|
8195
|
-
};
|
|
8196
|
-
} catch (error) {
|
|
8197
|
-
if (error instanceof TypeError) resolution = {
|
|
8198
|
-
type: "rejected",
|
|
8199
|
-
reason: `The document is malformed: ${error.message}`
|
|
8200
|
-
};
|
|
8201
|
-
else {
|
|
8202
|
-
logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
|
|
8211
|
+
try {
|
|
8212
|
+
for (const url of requestUrls) {
|
|
8213
|
+
if (signal.aborted) {
|
|
8214
|
+
logger.debug("Ran out of time looking up the actor of key {keyId}; gateways from {url} on were not tried.", {
|
|
8203
8215
|
keyId: formattedKeyId,
|
|
8204
|
-
url
|
|
8205
|
-
error
|
|
8216
|
+
url
|
|
8206
8217
|
});
|
|
8207
8218
|
attempts.push({
|
|
8208
8219
|
type: "indeterminate",
|
|
8209
|
-
error
|
|
8220
|
+
error: signal.reason
|
|
8221
|
+
});
|
|
8222
|
+
break;
|
|
8223
|
+
}
|
|
8224
|
+
const state = { processing: false };
|
|
8225
|
+
let attempt;
|
|
8226
|
+
try {
|
|
8227
|
+
attempt = await raceAbort(attemptGateway(url, keyId, attemptOptions, state), signal);
|
|
8228
|
+
} catch (error) {
|
|
8229
|
+
if (!signal.aborted) throw error;
|
|
8230
|
+
logger.debug("Ran out of time looking up the actor of key {keyId} at {url}.", {
|
|
8231
|
+
keyId: formattedKeyId,
|
|
8232
|
+
url
|
|
8233
|
+
});
|
|
8234
|
+
attempts.push({
|
|
8235
|
+
type: state.processing ? "indeterminate" : "retrievalFailed",
|
|
8236
|
+
error: signal.reason
|
|
8210
8237
|
});
|
|
8211
8238
|
continue;
|
|
8212
8239
|
}
|
|
8240
|
+
if (attempt.type === "verified") return attempt;
|
|
8241
|
+
attempts.push(attempt);
|
|
8213
8242
|
}
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
|
|
8217
|
-
keyId: formattedKeyId,
|
|
8218
|
-
url,
|
|
8219
|
-
reason
|
|
8220
|
-
});
|
|
8221
|
-
attempts.push({
|
|
8222
|
-
type: "rejected",
|
|
8223
|
-
reason
|
|
8224
|
-
});
|
|
8243
|
+
} finally {
|
|
8244
|
+
clearTimeout(timer);
|
|
8225
8245
|
}
|
|
8226
8246
|
const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
|
|
8227
8247
|
if (rejections.length === attempts.length) return reject(rejections.join(" "));
|
|
@@ -8242,6 +8262,139 @@ async function resolvePortableActorKey(keyId, options = {}) {
|
|
|
8242
8262
|
};
|
|
8243
8263
|
}
|
|
8244
8264
|
/**
|
|
8265
|
+
* Fetches the actor's document of a key at an `ap:` or `ap+ef61:` key ID from
|
|
8266
|
+
* one gateway, and tells whether it vouches for the key. It never rejects;
|
|
8267
|
+
* the caller races it against the lookup's timeout, and ignores what it
|
|
8268
|
+
* resolves to after the timeout.
|
|
8269
|
+
*/
|
|
8270
|
+
async function attemptGateway(url, keyId, options, state) {
|
|
8271
|
+
const { documentLoader, contextLoader, signal, formattedKeyId } = options;
|
|
8272
|
+
let remoteDocument;
|
|
8273
|
+
try {
|
|
8274
|
+
remoteDocument = await documentLoader(url);
|
|
8275
|
+
} catch (error) {
|
|
8276
|
+
logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
|
|
8277
|
+
keyId: formattedKeyId,
|
|
8278
|
+
url,
|
|
8279
|
+
error
|
|
8280
|
+
});
|
|
8281
|
+
return {
|
|
8282
|
+
type: "retrievalFailed",
|
|
8283
|
+
error
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
if (signal.aborted) return {
|
|
8287
|
+
type: "indeterminate",
|
|
8288
|
+
error: signal.reason
|
|
8289
|
+
};
|
|
8290
|
+
state.processing = true;
|
|
8291
|
+
let resolution;
|
|
8292
|
+
try {
|
|
8293
|
+
const object = await _fedify_vocab.Object.fromJsonLd(remoteDocument.document, {
|
|
8294
|
+
documentLoader,
|
|
8295
|
+
contextLoader,
|
|
8296
|
+
tracerProvider: options.tracerProvider
|
|
8297
|
+
});
|
|
8298
|
+
signal.throwIfAborted();
|
|
8299
|
+
resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, options) : {
|
|
8300
|
+
type: "rejected",
|
|
8301
|
+
reason: "The document is not a portable actor."
|
|
8302
|
+
};
|
|
8303
|
+
} catch (error) {
|
|
8304
|
+
if (error instanceof TypeError && !signal.aborted) resolution = {
|
|
8305
|
+
type: "rejected",
|
|
8306
|
+
reason: `The document is malformed: ${error.message}`
|
|
8307
|
+
};
|
|
8308
|
+
else {
|
|
8309
|
+
logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
|
|
8310
|
+
keyId: formattedKeyId,
|
|
8311
|
+
url,
|
|
8312
|
+
error
|
|
8313
|
+
});
|
|
8314
|
+
return {
|
|
8315
|
+
type: "indeterminate",
|
|
8316
|
+
error
|
|
8317
|
+
};
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
if (resolution.type === "verified") return resolution;
|
|
8321
|
+
const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
|
|
8322
|
+
logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
|
|
8323
|
+
keyId: formattedKeyId,
|
|
8324
|
+
url,
|
|
8325
|
+
reason
|
|
8326
|
+
});
|
|
8327
|
+
return {
|
|
8328
|
+
type: "rejected",
|
|
8329
|
+
reason
|
|
8330
|
+
};
|
|
8331
|
+
}
|
|
8332
|
+
/**
|
|
8333
|
+
* Wraps a document loader so that it gives up when the given signal aborts,
|
|
8334
|
+
* as well as when the signal in its own options does. The wrapped loader is
|
|
8335
|
+
* passed a signal that aborts in either case, but in case it ignores that
|
|
8336
|
+
* signal, what it resolves to after the abort is ignored, and the wrapper
|
|
8337
|
+
* rejects with the reason of the abort at once. After the abort, the wrapped
|
|
8338
|
+
* loader is not called at all.
|
|
8339
|
+
*/
|
|
8340
|
+
function withDeadline(loader, signal) {
|
|
8341
|
+
return (url, options) => {
|
|
8342
|
+
const combined = combineSignals(signal, options?.signal);
|
|
8343
|
+
return raceAbort(combined.signal.aborted ? Promise.reject(combined.signal.reason) : new Promise((resolve) => resolve(loader(url, {
|
|
8344
|
+
...options,
|
|
8345
|
+
signal: combined.signal
|
|
8346
|
+
}))), combined.signal).finally(combined.dispose);
|
|
8347
|
+
};
|
|
8348
|
+
}
|
|
8349
|
+
/**
|
|
8350
|
+
* Makes a signal that aborts when either of the given signals does, with
|
|
8351
|
+
* the reason of the one that aborted first. `AbortSignal.any()` is not
|
|
8352
|
+
* available on every supported runtime, e.g., Bun before 1.1.4.
|
|
8353
|
+
* @returns The signal, and a function that stops listening to the given
|
|
8354
|
+
* signals.
|
|
8355
|
+
*/
|
|
8356
|
+
function combineSignals(signal, other) {
|
|
8357
|
+
if (other == null || other === signal) return {
|
|
8358
|
+
signal,
|
|
8359
|
+
dispose() {}
|
|
8360
|
+
};
|
|
8361
|
+
const controller = new AbortController();
|
|
8362
|
+
const sources = [signal, other];
|
|
8363
|
+
const dispose = () => {
|
|
8364
|
+
for (const source of sources) source.removeEventListener("abort", onAbort);
|
|
8365
|
+
};
|
|
8366
|
+
function onAbort() {
|
|
8367
|
+
dispose();
|
|
8368
|
+
controller.abort(this.reason);
|
|
8369
|
+
}
|
|
8370
|
+
const aborted = sources.find((source) => source.aborted);
|
|
8371
|
+
if (aborted == null) for (const source of sources) source.addEventListener("abort", onAbort);
|
|
8372
|
+
else controller.abort(aborted.reason);
|
|
8373
|
+
return {
|
|
8374
|
+
signal: controller.signal,
|
|
8375
|
+
dispose
|
|
8376
|
+
};
|
|
8377
|
+
}
|
|
8378
|
+
/**
|
|
8379
|
+
* Waits for the given promise unless the given signal aborts first, in which
|
|
8380
|
+
* case it rejects with the reason of the abort. If the signal has aborted by
|
|
8381
|
+
* the time the promise settles, the abort wins as well, so that nothing that
|
|
8382
|
+
* settles after it counts.
|
|
8383
|
+
*/
|
|
8384
|
+
function raceAbort(promise, signal) {
|
|
8385
|
+
return new Promise((resolve, reject) => {
|
|
8386
|
+
const onAbort = () => reject(signal.reason);
|
|
8387
|
+
const settle = (settled) => {
|
|
8388
|
+
signal.removeEventListener("abort", onAbort);
|
|
8389
|
+
if (signal.aborted) onAbort();
|
|
8390
|
+
else settled();
|
|
8391
|
+
};
|
|
8392
|
+
promise.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
8393
|
+
if (signal.aborted) onAbort();
|
|
8394
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
8395
|
+
});
|
|
8396
|
+
}
|
|
8397
|
+
/**
|
|
8245
8398
|
* Finds the nodes that stand for the key with the given canonical portable
|
|
8246
8399
|
* ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
|
|
8247
8400
|
* itself, e.g., Mitra lists the keys of portable actors that way while
|
|
@@ -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;
|
|
@@ -4533,6 +4533,21 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
4533
4533
|
});
|
|
4534
4534
|
}
|
|
4535
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
|
+
/**
|
|
4536
4551
|
* Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
|
|
4537
4552
|
* `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
|
|
4538
4553
|
* the actor's document, i.e., the key ID without its fragment, is fetched
|
|
@@ -4560,12 +4575,25 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
4560
4575
|
* unreachable gateway would have served, so the result is an `unavailable`
|
|
4561
4576
|
* that is not cacheable.
|
|
4562
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
|
+
*
|
|
4563
4587
|
* @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
|
|
4564
|
-
* @param options Loaders and
|
|
4588
|
+
* @param options Loaders, telemetry providers, and the timeout.
|
|
4565
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.
|
|
4566
4592
|
* @internal
|
|
4567
4593
|
*/
|
|
4568
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}`);
|
|
4569
4597
|
const documentLoader = options.documentLoader ?? getDocumentLoader();
|
|
4570
4598
|
const contextLoader = options.contextLoader ?? getDocumentLoader();
|
|
4571
4599
|
let formattedKeyId = keyId.href;
|
|
@@ -4584,75 +4612,67 @@ async function resolvePortableActorKey(keyId, options = {}) {
|
|
|
4584
4612
|
if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
|
|
4585
4613
|
const actorUrl = new URL(keyId.href);
|
|
4586
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
|
+
});
|
|
4587
4620
|
const requestUrls = [];
|
|
4588
|
-
for (const gateway of
|
|
4621
|
+
for (const gateway of gateways.slice(0, MAX_KEY_ID_GATEWAY_HINTS)) try {
|
|
4589
4622
|
requestUrls.push(toCompatibleEf61Id(actorUrl, gateway).href);
|
|
4590
4623
|
} catch (error) {
|
|
4591
4624
|
if (!(error instanceof TypeError)) throw error;
|
|
4592
4625
|
return reject(`The key ID cannot be fetched from gateways: ${error}`);
|
|
4593
4626
|
}
|
|
4594
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
|
+
};
|
|
4595
4641
|
const attempts = [];
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
} catch (error) {
|
|
4601
|
-
logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
|
|
4602
|
-
keyId: formattedKeyId,
|
|
4603
|
-
url,
|
|
4604
|
-
error
|
|
4605
|
-
});
|
|
4606
|
-
attempts.push({
|
|
4607
|
-
type: "retrievalFailed",
|
|
4608
|
-
error
|
|
4609
|
-
});
|
|
4610
|
-
continue;
|
|
4611
|
-
}
|
|
4612
|
-
let resolution;
|
|
4613
|
-
try {
|
|
4614
|
-
const object = await Object$1.fromJsonLd(remoteDocument.document, {
|
|
4615
|
-
documentLoader,
|
|
4616
|
-
contextLoader,
|
|
4617
|
-
tracerProvider: options.tracerProvider
|
|
4618
|
-
});
|
|
4619
|
-
resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
|
|
4620
|
-
...options,
|
|
4621
|
-
documentLoader,
|
|
4622
|
-
contextLoader
|
|
4623
|
-
}) : {
|
|
4624
|
-
type: "rejected",
|
|
4625
|
-
reason: "The document is not a portable actor."
|
|
4626
|
-
};
|
|
4627
|
-
} catch (error) {
|
|
4628
|
-
if (error instanceof TypeError) resolution = {
|
|
4629
|
-
type: "rejected",
|
|
4630
|
-
reason: `The document is malformed: ${error.message}`
|
|
4631
|
-
};
|
|
4632
|
-
else {
|
|
4633
|
-
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.", {
|
|
4634
4646
|
keyId: formattedKeyId,
|
|
4635
|
-
url
|
|
4636
|
-
error
|
|
4647
|
+
url
|
|
4637
4648
|
});
|
|
4638
4649
|
attempts.push({
|
|
4639
4650
|
type: "indeterminate",
|
|
4640
|
-
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
|
|
4641
4668
|
});
|
|
4642
4669
|
continue;
|
|
4643
4670
|
}
|
|
4671
|
+
if (attempt.type === "verified") return attempt;
|
|
4672
|
+
attempts.push(attempt);
|
|
4644
4673
|
}
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
|
|
4648
|
-
keyId: formattedKeyId,
|
|
4649
|
-
url,
|
|
4650
|
-
reason
|
|
4651
|
-
});
|
|
4652
|
-
attempts.push({
|
|
4653
|
-
type: "rejected",
|
|
4654
|
-
reason
|
|
4655
|
-
});
|
|
4674
|
+
} finally {
|
|
4675
|
+
clearTimeout(timer);
|
|
4656
4676
|
}
|
|
4657
4677
|
const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
|
|
4658
4678
|
if (rejections.length === attempts.length) return reject(rejections.join(" "));
|
|
@@ -4673,6 +4693,139 @@ async function resolvePortableActorKey(keyId, options = {}) {
|
|
|
4673
4693
|
};
|
|
4674
4694
|
}
|
|
4675
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
|
+
/**
|
|
4676
4829
|
* Finds the nodes that stand for the key with the given canonical portable
|
|
4677
4830
|
* ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
|
|
4678
4831
|
* itself, e.g., Mitra lists the keys of portable actors that way while
|
|
@@ -1,13 +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
8
|
import { a as promoteKeyOwnerEvidence } from "./key-owner-evidence-Bo3yaeL5.mjs";
|
|
9
|
-
import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-
|
|
10
|
-
import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-
|
|
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";
|
|
11
11
|
import { getLogger } from "@logtape/logtape";
|
|
12
12
|
import { CryptographicKey } from "@fedify/vocab";
|
|
13
13
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
@@ -1,8 +1,8 @@
|
|
|
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 { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-
|
|
4
|
+
import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
|
|
5
|
+
import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-jUDFfVgi.mjs";
|
|
6
6
|
import { c as isPortableId, f as parseKeyIdString, o as isCompatibleKeyId, s as isPortableActorDocument, u as isPortableUri } from "./portable-key-id-Bg259c4Z.mjs";
|
|
7
7
|
import { n as attachKeyOwnerEvidence, t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-Bo3yaeL5.mjs";
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { p as recordDocumentCache } from "./metrics-
|
|
4
|
+
import { p as recordDocumentCache } from "./metrics-jUDFfVgi.mjs";
|
|
5
5
|
import { getLogger } from "@logtape/logtape";
|
|
6
6
|
import { preloadedContexts } from "@fedify/vocab-runtime";
|
|
7
7
|
//#region src/utils/kv-cache.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
|
-
const require_http = require("./http-
|
|
2
|
+
const require_http = require("./http-5CJBdU8n.cjs");
|
|
3
3
|
let _logtape_logtape = require("@logtape/logtape");
|
|
4
4
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
5
5
|
let es_toolkit = require("es-toolkit");
|