@fedify/fedify 2.3.0-dev.1099 → 2.3.0-dev.1114
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/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DQP-q39h.mjs} +27 -2
- package/dist/{builder-BkRRjxzb.mjs → builder-YlEusQth.mjs} +3 -3
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +1 -1
- package/dist/compat/public-audience.test.mjs +1 -1
- package/dist/compat/transformers.test.mjs +2 -2
- package/dist/{context-C0C_sRha.d.cts → context-Ch-ZLyTQ.d.cts} +1 -1
- package/dist/{context-Dqgt8saU.d.ts → context-cSUMk2da.d.ts} +1 -1
- package/dist/{deno-DBabeupC.mjs → deno-CF3jMgip.mjs} +1 -1
- package/dist/{docloader-DA5FzJOR.mjs → docloader-BENj6vQ4.mjs} +2 -2
- package/dist/federation/builder.test.mjs +3 -3
- package/dist/federation/collection.test.mjs +2 -2
- package/dist/federation/handler.test.mjs +8 -7
- package/dist/federation/idempotency.test.mjs +5 -5
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/keycache.test.mjs +1 -1
- package/dist/federation/kv.test.mjs +2 -2
- package/dist/federation/middleware.test.mjs +10 -10
- 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/mq.test.mjs +2 -2
- package/dist/federation/negotiation.test.mjs +2 -2
- package/dist/federation/router.test.mjs +2 -2
- package/dist/federation/send.test.mjs +11 -11
- package/dist/federation/webfinger.test.mjs +3 -3
- package/dist/{getMachineId-bsd-etIyxDet.mjs → getMachineId-bsd-BY01PL1n.mjs} +1 -1
- package/dist/{getMachineId-darwin-D23zTf4g.mjs → getMachineId-darwin-Dr1gkBkp.mjs} +1 -1
- package/dist/{getMachineId-win-Dpap6v5i.mjs → getMachineId-win-QEYwcJiy.mjs} +1 -1
- package/dist/{http-5G18W3NP.mjs → http-BmOZYc-8.mjs} +86 -37
- package/dist/{http-W2u_KBoQ.cjs → http-CKCgOPkX.cjs} +427 -35
- package/dist/{http-Dzy5c472.js → http-CpzZ9zsb.js} +393 -37
- package/dist/{http-BDZeS5om.d.ts → http-D6LP89UO.d.ts} +7 -1
- package/dist/{http-C87EWkO0.d.cts → http-D6aw3j2U.d.cts} +7 -1
- package/dist/{key-D9dUsyow.mjs → key-B4I8H5Lc.mjs} +1 -1
- package/dist/{kv-cache-BygrlQ1c.cjs → kv-cache-DY-XWOqM.cjs} +1 -1
- package/dist/{kv-cache-CBSgxEsZ.js → kv-cache-Wc5ezcVW.js} +1 -1
- package/dist/{ld-hbxDLO1k.mjs → ld-B5D5THhl.mjs} +60 -9
- package/dist/{send-BOwz4Hw5.mjs → metrics-ek3ilf6c.mjs} +53 -221
- package/dist/{middleware-vCF_cKAq.js → middleware-CuZbBw-N.js} +16 -269
- package/dist/{middleware-BXnhAGF9.mjs → middleware-DlcecZMq.mjs} +29 -23
- package/dist/{middleware-DZQsPMZb.mjs → middleware-EI7OU6BR.mjs} +1 -1
- package/dist/{middleware-Caj827xW.cjs → middleware-EqTYPG4F.cjs} +45 -298
- package/dist/{mod-DXY9JF28.d.cts → mod-B-Lin9Sy.d.ts} +25 -2
- package/dist/{mod-DHO9lk3D.d.ts → mod-BDhgfjP7.d.cts} +25 -2
- package/dist/{mod-B0rWmfW5.d.cts → mod-BR_BB0bh.d.cts} +1 -1
- package/dist/{mod-Dx3-hqyo.d.ts → mod-C6E8rkcz.d.ts} +1 -1
- package/dist/{mod-BhU_H1I_.d.ts → mod-DLrRb0dx.d.ts} +1 -1
- package/dist/{mod-CLPnQPsv.d.cts → mod-P9tE2WmM.d.cts} +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/client.test.mjs +2 -2
- package/dist/nodeinfo/handler.test.mjs +3 -3
- package/dist/nodeinfo/types.test.mjs +2 -2
- package/dist/otel/exporter.test.mjs +2 -2
- package/dist/{outgoing-jsonld-BgFLCJQ_.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
- package/dist/{owner-DwJe0BH9.mjs → owner-DO810N24.mjs} +2 -2
- package/dist/{proof-erpV_J_n.mjs → proof-BgfyWv7b.mjs} +25 -7
- package/dist/{proof-CZCaAURh.cjs → proof-DIoqrKnX.cjs} +78 -11
- package/dist/{proof-DMJJZnKd.js → proof-Vd8-1EWh.js} +78 -11
- package/dist/send-CAYXdUTk.mjs +225 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/http.test.mjs +212 -6
- package/dist/sig/key.test.mjs +4 -4
- package/dist/sig/ld.test.mjs +138 -5
- 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 +4 -4
- package/dist/sig/proof.test.mjs +167 -6
- package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BTEgfoJo.mjs} +2 -27
- package/dist/utils/docloader.test.mjs +5 -5
- 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 +6 -6
- /package/dist/{accept-CceiKpCy.mjs → accept-CgDcxvjV.mjs} +0 -0
- /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
- /package/dist/{client-B_A6mfn3.mjs → client-Bneh_DYR.mjs} +0 -0
- /package/dist/{collection-CA3V5zyK.mjs → collection-Cc3DVAhE.mjs} +0 -0
- /package/dist/{execAsync-DCBrgFiV.mjs → execAsync-Dxb7rNf3.mjs} +0 -0
- /package/dist/{getMachineId-linux-ObI47Hql.mjs → getMachineId-linux-Bbhofx-s.mjs} +0 -0
- /package/dist/{getMachineId-unsupported-Ddu-PFeh.mjs → getMachineId-unsupported-dIOte2Ct.mjs} +0 -0
- /package/dist/{keys-C3kae-6B.mjs → keys-CSYsOMFG.mjs} +0 -0
- /package/dist/{kv-x2IvBUyq.mjs → kv-QHE0oeM3.mjs} +0 -0
- /package/dist/{kv-cache-CiiNwT6W.mjs → kv-cache-DihufyAQ.mjs} +0 -0
- /package/dist/{public-audience-N3pyOx2p.mjs → public-audience-c9zmYKgA.mjs} +0 -0
- /package/dist/{types-BFowWFTT.mjs → types-D09GN0uZ.mjs} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
|
-
import { d as validateCryptoKey, t as doubleKnock } from "./http-
|
|
3
|
+
import { d as validateCryptoKey, t as doubleKnock } from "./http-CpzZ9zsb.js";
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
5
|
import { curry } from "es-toolkit";
|
|
6
6
|
import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import "@js-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
|
|
4
|
+
import { n as version, t as name } from "./deno-CF3jMgip.mjs";
|
|
5
|
+
import { a as measureSignatureKeyFetch, n as getFederationMetrics, t as getDurationMs } from "./metrics-ek3ilf6c.mjs";
|
|
6
|
+
import { n as fetchKey, o as validateCryptoKey } from "./key-B4I8H5Lc.mjs";
|
|
6
7
|
import { getLogger } from "@logtape/logtape";
|
|
7
8
|
import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
|
|
8
9
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
@@ -146,6 +147,17 @@ function detachSignature(jsonLd) {
|
|
|
146
147
|
}
|
|
147
148
|
/**
|
|
148
149
|
* Verifies Linked Data Signatures of the given JSON-LD document.
|
|
150
|
+
*
|
|
151
|
+
* This is a low-level utility that only checks the cryptographic signature
|
|
152
|
+
* and (optionally) the cached key. It does not run the JSON-LD parsing,
|
|
153
|
+
* attribution, and owner checks that a complete inbound LD verification
|
|
154
|
+
* needs. For incoming activities, prefer {@link verifyJsonLd}, which is
|
|
155
|
+
* the public verification entry point and the one that emits the
|
|
156
|
+
* `activitypub.signature.verification.duration` metric for the LD path.
|
|
157
|
+
* `verifySignature` itself only emits
|
|
158
|
+
* `activitypub.signature.key_fetch.duration`, since the rest of the work
|
|
159
|
+
* that the verification-duration metric is meant to cover happens in
|
|
160
|
+
* `verifyJsonLd`.
|
|
149
161
|
* @param jsonLd The JSON-LD document to verify.
|
|
150
162
|
* @param options Options for verifying the signature.
|
|
151
163
|
* @returns The public key that signed the document or `null` if the signature
|
|
@@ -165,7 +177,7 @@ async function verifySignature(jsonLd, options = {}) {
|
|
|
165
177
|
});
|
|
166
178
|
return null;
|
|
167
179
|
}
|
|
168
|
-
const { key, cached } = await fetchKey(new URL(sig.creator), CryptographicKey, options);
|
|
180
|
+
const { key, cached } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, options));
|
|
169
181
|
if (key == null) return null;
|
|
170
182
|
const sigOpts = {
|
|
171
183
|
...sig,
|
|
@@ -205,13 +217,13 @@ async function verifySignature(jsonLd, options = {}) {
|
|
|
205
217
|
keyId: sig.creator,
|
|
206
218
|
...sig
|
|
207
219
|
});
|
|
208
|
-
const { key } = await fetchKey(new URL(sig.creator), CryptographicKey, {
|
|
220
|
+
const { key } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, {
|
|
209
221
|
...options,
|
|
210
222
|
keyCache: {
|
|
211
223
|
get: () => Promise.resolve(void 0),
|
|
212
224
|
set: async (keyId, key) => await options.keyCache?.set(keyId, key)
|
|
213
225
|
}
|
|
214
|
-
});
|
|
226
|
+
}));
|
|
215
227
|
if (key == null) return null;
|
|
216
228
|
return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
|
|
217
229
|
}
|
|
@@ -223,6 +235,33 @@ async function verifySignature(jsonLd, options = {}) {
|
|
|
223
235
|
return null;
|
|
224
236
|
}
|
|
225
237
|
/**
|
|
238
|
+
* Known Linked Data Signature `type` values, used to keep
|
|
239
|
+
* `ld_signatures.type` on a bounded set of spec-defined string values.
|
|
240
|
+
* Fedify only signs and verifies `RsaSignature2017`; other values come in
|
|
241
|
+
* only from external documents and are dropped from the metric attribute to
|
|
242
|
+
* avoid attacker-controlled cardinality.
|
|
243
|
+
*/
|
|
244
|
+
const LD_KNOWN_SIGNATURE_TYPES = new Set(["RsaSignature2017"]);
|
|
245
|
+
/**
|
|
246
|
+
* Reports only whether a `signature` key is present on the document, with
|
|
247
|
+
* no shape check on its value. This is intentionally looser than
|
|
248
|
+
* {@link hasSignature} (which validates a full `RsaSignature2017` shape)
|
|
249
|
+
* and {@link hasSignatureLike} (which structurally accepts several known
|
|
250
|
+
* suites): `verifyJsonLd` needs to tell a document with a malformed or
|
|
251
|
+
* unsupported signature payload (classified as `rejected`) apart from a
|
|
252
|
+
* truly unsigned document (classified as `missing`), and only this
|
|
253
|
+
* presence-only check captures both cases.
|
|
254
|
+
*/
|
|
255
|
+
function hasLdSignatureProperty(jsonLd) {
|
|
256
|
+
return typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd;
|
|
257
|
+
}
|
|
258
|
+
function getLdSignatureObject(jsonLd) {
|
|
259
|
+
if (!hasLdSignatureProperty(jsonLd)) return void 0;
|
|
260
|
+
const { signature } = jsonLd;
|
|
261
|
+
if (typeof signature !== "object" || signature == null || Array.isArray(signature)) return;
|
|
262
|
+
return signature;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
226
265
|
* Verify the authenticity of the given JSON-LD document using Linked Data
|
|
227
266
|
* Signatures. If the document is signed, this function verifies the signature
|
|
228
267
|
* and checks if the document is attributed to the owner of the public key.
|
|
@@ -233,14 +272,22 @@ async function verifySignature(jsonLd, options = {}) {
|
|
|
233
272
|
*/
|
|
234
273
|
async function verifyJsonLd(jsonLd, options = {}) {
|
|
235
274
|
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("ld_signatures.verify", async (span) => {
|
|
275
|
+
const start = performance.now();
|
|
276
|
+
let verified = false;
|
|
277
|
+
let threw = false;
|
|
278
|
+
let signatureType;
|
|
236
279
|
try {
|
|
237
280
|
const object = await Object$1.fromJsonLd(jsonLd, options);
|
|
238
281
|
if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
|
|
239
282
|
span.setAttribute("activitypub.object.type", getTypeId(object).href);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
if (
|
|
283
|
+
const sig = getLdSignatureObject(jsonLd);
|
|
284
|
+
if (sig != null) {
|
|
285
|
+
if (typeof sig.creator === "string") span.setAttribute("ld_signatures.key_id", sig.creator);
|
|
286
|
+
if (typeof sig.signatureValue === "string") span.setAttribute("ld_signatures.signature", sig.signatureValue);
|
|
287
|
+
if (typeof sig.type === "string") {
|
|
288
|
+
span.setAttribute("ld_signatures.type", sig.type);
|
|
289
|
+
if (LD_KNOWN_SIGNATURE_TYPES.has(sig.type)) signatureType = sig.type;
|
|
290
|
+
}
|
|
244
291
|
}
|
|
245
292
|
const attributions = new Set(object.attributionIds.map((uri) => uri.href));
|
|
246
293
|
if (object instanceof Activity) for (const uri of object.actorIds) attributions.add(uri.href);
|
|
@@ -255,14 +302,18 @@ async function verifyJsonLd(jsonLd, options = {}) {
|
|
|
255
302
|
logger.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
|
|
256
303
|
return false;
|
|
257
304
|
}
|
|
305
|
+
verified = true;
|
|
258
306
|
return true;
|
|
259
307
|
} catch (error) {
|
|
308
|
+
threw = true;
|
|
260
309
|
span.setStatus({
|
|
261
310
|
code: SpanStatusCode.ERROR,
|
|
262
311
|
message: String(error)
|
|
263
312
|
});
|
|
264
313
|
throw error;
|
|
265
314
|
} finally {
|
|
315
|
+
const classified = threw ? "error" : verified ? "verified" : hasLdSignatureProperty(jsonLd) ? "rejected" : "missing";
|
|
316
|
+
getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "linked_data", classified, { ldType: signatureType });
|
|
266
317
|
span.end();
|
|
267
318
|
}
|
|
268
319
|
});
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as version, t as name } from "./deno-
|
|
5
|
-
import {
|
|
6
|
-
import { getLogger } from "@logtape/logtape";
|
|
7
|
-
import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
4
|
+
import { n as version, t as name } from "./deno-CF3jMgip.mjs";
|
|
5
|
+
import { metrics } from "@opentelemetry/api";
|
|
8
6
|
//#region src/federation/metrics.ts
|
|
9
7
|
var FederationMetrics = class {
|
|
10
8
|
deliverySent;
|
|
11
9
|
deliveryPermanentFailure;
|
|
12
10
|
signatureVerificationFailure;
|
|
11
|
+
signatureVerificationDuration;
|
|
12
|
+
signatureKeyFetchDuration;
|
|
13
13
|
deliveryDuration;
|
|
14
14
|
inboxProcessingDuration;
|
|
15
15
|
httpServerRequestCount;
|
|
@@ -34,6 +34,14 @@ var FederationMetrics = class {
|
|
|
34
34
|
description: "ActivityPub signature verification failures.",
|
|
35
35
|
unit: "{failure}"
|
|
36
36
|
});
|
|
37
|
+
this.signatureVerificationDuration = meter.createHistogram("activitypub.signature.verification.duration", {
|
|
38
|
+
description: "Duration of ActivityPub signature verification, including local key lookup and remote key fetches.",
|
|
39
|
+
unit: "ms"
|
|
40
|
+
});
|
|
41
|
+
this.signatureKeyFetchDuration = meter.createHistogram("activitypub.signature.key_fetch.duration", {
|
|
42
|
+
description: "Duration of public key lookup performed during ActivityPub signature verification.",
|
|
43
|
+
unit: "ms"
|
|
44
|
+
});
|
|
37
45
|
this.deliveryDuration = meter.createHistogram("activitypub.delivery.duration", {
|
|
38
46
|
description: "Duration of ActivityPub delivery attempts.",
|
|
39
47
|
unit: "ms"
|
|
@@ -127,6 +135,23 @@ var FederationMetrics = class {
|
|
|
127
135
|
if (remoteHost != null) attributes["activitypub.remote.host"] = remoteHost;
|
|
128
136
|
this.signatureVerificationFailure.add(1, attributes);
|
|
129
137
|
}
|
|
138
|
+
recordSignatureVerificationDuration(durationMs, kind, result, extra = {}) {
|
|
139
|
+
const attributes = {
|
|
140
|
+
"activitypub.signature.kind": kind,
|
|
141
|
+
"activitypub.signature.result": result
|
|
142
|
+
};
|
|
143
|
+
if (extra.algorithm != null) attributes["http_signatures.algorithm"] = extra.algorithm;
|
|
144
|
+
if (extra.failureReason != null) attributes["http_signatures.failure_reason"] = extra.failureReason;
|
|
145
|
+
if (extra.ldType != null) attributes["ld_signatures.type"] = extra.ldType;
|
|
146
|
+
if (extra.cryptosuite != null) attributes["object_integrity_proofs.cryptosuite"] = extra.cryptosuite;
|
|
147
|
+
this.signatureVerificationDuration.record(durationMs, attributes);
|
|
148
|
+
}
|
|
149
|
+
recordSignatureKeyFetchDuration(durationMs, kind, result) {
|
|
150
|
+
this.signatureKeyFetchDuration.record(durationMs, {
|
|
151
|
+
"activitypub.signature.kind": kind,
|
|
152
|
+
"activitypub.signature.key_fetch.result": result
|
|
153
|
+
});
|
|
154
|
+
}
|
|
130
155
|
recordInboxProcessingDuration(activityType, durationMs) {
|
|
131
156
|
this.inboxProcessingDuration.record(durationMs, { "activitypub.activity.type": activityType });
|
|
132
157
|
}
|
|
@@ -207,6 +232,29 @@ function recordOutboxEnqueue(meterProvider, outboxQueue, message) {
|
|
|
207
232
|
}, message.attempt);
|
|
208
233
|
}
|
|
209
234
|
/**
|
|
235
|
+
* Times an awaited public key fetch and records exactly one
|
|
236
|
+
* `activitypub.signature.key_fetch.duration` measurement, classifying the
|
|
237
|
+
* outcome as `hit`, `fetched`, or `error` based on the `cached` flag and
|
|
238
|
+
* whether the returned key is non-null. Errors thrown by the fetch are
|
|
239
|
+
* reported as `error` and rethrown, so verifier behavior is unchanged.
|
|
240
|
+
*
|
|
241
|
+
* Shared by the three signature verifiers (HTTP, Linked Data, Object
|
|
242
|
+
* Integrity Proofs); the only per-call variation is the
|
|
243
|
+
* `activitypub.signature.kind` attribute value.
|
|
244
|
+
* @since 2.3.0
|
|
245
|
+
*/
|
|
246
|
+
async function measureSignatureKeyFetch(meterProvider, kind, fetch) {
|
|
247
|
+
const start = performance.now();
|
|
248
|
+
try {
|
|
249
|
+
const result = await fetch();
|
|
250
|
+
getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, result.key != null ? result.cached ? "hit" : "fetched" : "error");
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, "error");
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
210
258
|
* Whether the given thrown value is an `AbortError`.
|
|
211
259
|
*
|
|
212
260
|
* `processQueuedTask` distinguishes aborted tasks (recorded as
|
|
@@ -263,220 +311,4 @@ function getDurationMs(start) {
|
|
|
263
311
|
return Math.max(0, performance.now() - start);
|
|
264
312
|
}
|
|
265
313
|
//#endregion
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Extracts the inbox URLs from recipients.
|
|
269
|
-
* @param parameters The parameters to extract the inboxes.
|
|
270
|
-
* See also {@link ExtractInboxesParameters}.
|
|
271
|
-
* @returns The inboxes as a map of inbox URL to actor URIs.
|
|
272
|
-
*/
|
|
273
|
-
function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
|
274
|
-
const inboxes = {};
|
|
275
|
-
for (const recipient of recipients) {
|
|
276
|
-
let inbox;
|
|
277
|
-
let sharedInbox = false;
|
|
278
|
-
if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
|
|
279
|
-
inbox = recipient.endpoints.sharedInbox;
|
|
280
|
-
sharedInbox = true;
|
|
281
|
-
} else inbox = recipient.inboxId;
|
|
282
|
-
if (inbox != null && recipient.id != null) {
|
|
283
|
-
if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
|
|
284
|
-
inboxes[inbox.href] ??= {
|
|
285
|
-
actorIds: /* @__PURE__ */ new Set(),
|
|
286
|
-
sharedInbox
|
|
287
|
-
};
|
|
288
|
-
inboxes[inbox.href].actorIds.add(recipient.id.href);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return inboxes;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Sends an {@link Activity} to an inbox.
|
|
295
|
-
*
|
|
296
|
-
* @param parameters The parameters for sending the activity.
|
|
297
|
-
* See also {@link SendActivityParameters}.
|
|
298
|
-
* @throws {Error} If the activity fails to send.
|
|
299
|
-
*/
|
|
300
|
-
function sendActivity(options) {
|
|
301
|
-
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
302
|
-
return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
|
|
303
|
-
kind: SpanKind.CLIENT,
|
|
304
|
-
attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
|
|
305
|
-
}, async (span) => {
|
|
306
|
-
if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
|
|
307
|
-
if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
|
|
308
|
-
try {
|
|
309
|
-
await sendActivityInternal({
|
|
310
|
-
...options,
|
|
311
|
-
tracerProvider
|
|
312
|
-
}, span);
|
|
313
|
-
} catch (e) {
|
|
314
|
-
span.setStatus({
|
|
315
|
-
code: SpanStatusCode.ERROR,
|
|
316
|
-
message: String(e)
|
|
317
|
-
});
|
|
318
|
-
throw e;
|
|
319
|
-
} finally {
|
|
320
|
-
span.end();
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
|
|
325
|
-
function getActivityActorId(activity) {
|
|
326
|
-
if (!isRecord(activity)) return void 0;
|
|
327
|
-
return getIdValue(activity.actor);
|
|
328
|
-
}
|
|
329
|
-
function getIdValue(value) {
|
|
330
|
-
if (typeof value === "string" && value !== "") return value;
|
|
331
|
-
if (value instanceof URL) return value.href;
|
|
332
|
-
if (Array.isArray(value)) {
|
|
333
|
-
for (const item of value) {
|
|
334
|
-
const id = getIdValue(item);
|
|
335
|
-
if (id != null) return id;
|
|
336
|
-
}
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (isRecord(value)) return getIdValue(value.id);
|
|
340
|
-
}
|
|
341
|
-
function isRecord(value) {
|
|
342
|
-
return typeof value === "object" && value != null;
|
|
343
|
-
}
|
|
344
|
-
async function readLimitedResponseBody(response, maxBytes) {
|
|
345
|
-
if (response.body == null) return "";
|
|
346
|
-
const reader = response.body.getReader();
|
|
347
|
-
const decoder = new TextDecoder();
|
|
348
|
-
const chunks = [];
|
|
349
|
-
let totalBytes = 0;
|
|
350
|
-
let truncated = false;
|
|
351
|
-
try {
|
|
352
|
-
while (true) {
|
|
353
|
-
const { done, value } = await reader.read();
|
|
354
|
-
if (done) break;
|
|
355
|
-
if (totalBytes + value.length > maxBytes) {
|
|
356
|
-
const remaining = maxBytes - totalBytes;
|
|
357
|
-
if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
|
|
358
|
-
truncated = true;
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
chunks.push(decoder.decode(value, { stream: true }));
|
|
362
|
-
totalBytes += value.length;
|
|
363
|
-
}
|
|
364
|
-
} finally {
|
|
365
|
-
reader.releaseLock();
|
|
366
|
-
}
|
|
367
|
-
let result = chunks.join("");
|
|
368
|
-
if (truncated) result += "… (truncated)";
|
|
369
|
-
return result;
|
|
370
|
-
}
|
|
371
|
-
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
372
|
-
const logger = getLogger([
|
|
373
|
-
"fedify",
|
|
374
|
-
"federation",
|
|
375
|
-
"outbox"
|
|
376
|
-
]);
|
|
377
|
-
const federationMetrics = getFederationMetrics(meterProvider);
|
|
378
|
-
const started = performance.now();
|
|
379
|
-
let deliverySuccess = false;
|
|
380
|
-
headers = new Headers(headers);
|
|
381
|
-
headers.set("Content-Type", "application/activity+json");
|
|
382
|
-
const request = new Request(inbox, {
|
|
383
|
-
method: "POST",
|
|
384
|
-
headers,
|
|
385
|
-
body: JSON.stringify(activity)
|
|
386
|
-
});
|
|
387
|
-
let rsaKey = null;
|
|
388
|
-
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
389
|
-
rsaKey = key;
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
|
393
|
-
inbox: inbox.href,
|
|
394
|
-
keys: keys.map((pair) => ({
|
|
395
|
-
keyId: pair.keyId.href,
|
|
396
|
-
privateKey: pair.privateKey
|
|
397
|
-
}))
|
|
398
|
-
});
|
|
399
|
-
let response;
|
|
400
|
-
try {
|
|
401
|
-
response = rsaKey == null ? await fetch(request) : await doubleKnock(request, rsaKey, {
|
|
402
|
-
tracerProvider,
|
|
403
|
-
specDeterminer
|
|
404
|
-
});
|
|
405
|
-
} catch (error) {
|
|
406
|
-
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
407
|
-
activityId,
|
|
408
|
-
inbox: inbox.href,
|
|
409
|
-
error
|
|
410
|
-
});
|
|
411
|
-
federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
|
|
412
|
-
throw error;
|
|
413
|
-
}
|
|
414
|
-
try {
|
|
415
|
-
if (!response.ok) {
|
|
416
|
-
let error;
|
|
417
|
-
try {
|
|
418
|
-
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
419
|
-
} catch (_) {
|
|
420
|
-
error = "";
|
|
421
|
-
}
|
|
422
|
-
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
423
|
-
activityId,
|
|
424
|
-
inbox: inbox.href,
|
|
425
|
-
status: response.status,
|
|
426
|
-
statusText: response.statusText,
|
|
427
|
-
error
|
|
428
|
-
});
|
|
429
|
-
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
|
|
430
|
-
}
|
|
431
|
-
deliverySuccess = true;
|
|
432
|
-
const eventAttributes = {
|
|
433
|
-
"activitypub.inbox.url": inbox.href,
|
|
434
|
-
"activitypub.activity.id": activityId ?? ""
|
|
435
|
-
};
|
|
436
|
-
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
437
|
-
const actorId = getActivityActorId(activity);
|
|
438
|
-
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
439
|
-
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
440
|
-
} finally {
|
|
441
|
-
federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
446
|
-
* It contains structured information about the failure, including the HTTP
|
|
447
|
-
* status code, the inbox URL, and the response body.
|
|
448
|
-
* @since 2.0.0
|
|
449
|
-
*/
|
|
450
|
-
var SendActivityError = class extends Error {
|
|
451
|
-
/**
|
|
452
|
-
* The inbox URL that the activity was being sent to.
|
|
453
|
-
*/
|
|
454
|
-
inbox;
|
|
455
|
-
/**
|
|
456
|
-
* The HTTP status code returned by the inbox.
|
|
457
|
-
*/
|
|
458
|
-
statusCode;
|
|
459
|
-
/**
|
|
460
|
-
* The response body from the inbox, if any. Note that this may be
|
|
461
|
-
* truncated to a maximum of 1 KiB to prevent excessive memory consumption
|
|
462
|
-
* when remote servers return large error pages (e.g., Cloudflare error pages).
|
|
463
|
-
* If truncated, the string will end with `"… (truncated)"`.
|
|
464
|
-
*/
|
|
465
|
-
responseBody;
|
|
466
|
-
/**
|
|
467
|
-
* Creates a new {@link SendActivityError}.
|
|
468
|
-
* @param inbox The inbox URL.
|
|
469
|
-
* @param statusCode The HTTP status code.
|
|
470
|
-
* @param message The error message.
|
|
471
|
-
* @param responseBody The response body.
|
|
472
|
-
*/
|
|
473
|
-
constructor(inbox, statusCode, message, responseBody) {
|
|
474
|
-
super(message);
|
|
475
|
-
this.name = "SendActivityError";
|
|
476
|
-
this.inbox = inbox;
|
|
477
|
-
this.statusCode = statusCode;
|
|
478
|
-
this.responseBody = responseBody;
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
//#endregion
|
|
482
|
-
export { getFederationMetrics as a, recordOutboxEnqueue as c, getDurationMs as i, extractInboxes as n, getRemoteHost as o, sendActivity as r, isAbortError as s, SendActivityError as t };
|
|
314
|
+
export { measureSignatureKeyFetch as a, isAbortError as i, getFederationMetrics as n, recordOutboxEnqueue as o, getRemoteHost as r, getDurationMs as t };
|