@fedify/fedify 2.3.12-dev.2365 → 2.3.13

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.
Files changed (68) hide show
  1. package/dist/{assert_rejects-DN60FHPX.mjs → assert_rejects-D1HYAik3.mjs} +2 -20
  2. package/dist/{assert_throws-BOkhLGYc.mjs → assert_throws-BbSMEFKo.mjs} +19 -1
  3. package/dist/{builder-BOLaWLNy.mjs → builder-BagNDcQw.mjs} +2 -2
  4. package/dist/compat/transformers.test.mjs +2 -2
  5. package/dist/{deno-DSS6EkPl.mjs → deno-XpcjbHwj.mjs} +1 -1
  6. package/dist/{docloader-Ds5k4A6d.mjs → docloader-QDpF5t9M.mjs} +4 -2
  7. package/dist/federation/builder.test.mjs +3 -3
  8. package/dist/federation/circuit-breaker.test.mjs +3 -3
  9. package/dist/federation/collection.test.mjs +1 -1
  10. package/dist/federation/handler.test.mjs +262 -9
  11. package/dist/federation/idempotency.test.mjs +3 -3
  12. package/dist/federation/inbox.test.mjs +25 -1
  13. package/dist/federation/keycache.test.mjs +1 -1
  14. package/dist/federation/kv.test.mjs +1 -1
  15. package/dist/federation/metrics.test.mjs +3 -3
  16. package/dist/federation/middleware.test.mjs +177 -15
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.js +1 -1
  19. package/dist/federation/mq.test.mjs +4 -3
  20. package/dist/federation/negotiation.test.mjs +4 -4
  21. package/dist/federation/retry.test.mjs +1 -1
  22. package/dist/federation/router.test.mjs +3 -4
  23. package/dist/federation/send.test.mjs +7 -6
  24. package/dist/federation/temporal.test.mjs +4 -4
  25. package/dist/federation/webfinger.test.mjs +2 -2
  26. package/dist/{http-D9h6mQdq.mjs → http-BRrpIDS1.mjs} +3 -3
  27. package/dist/{http-DdaLQHim.cjs → http-DwPnG87d.cjs} +109 -11
  28. package/dist/{http-LMsqGo8j.js → http-GHVGCeb5.js} +110 -12
  29. package/dist/inbox-BzOuCPLY.mjs +180 -0
  30. package/dist/{key-vnmAGZ3_.mjs → key-PdEu1lDF.mjs} +111 -13
  31. package/dist/{kv-cache-Dz7_SbVI.js → kv-cache-BmqdF048.js} +3 -1
  32. package/dist/{kv-cache-CQuscHxa.mjs → kv-cache-Clx3SwMp.mjs} +1 -1
  33. package/dist/{kv-cache-ClCz-B0L.cjs → kv-cache-poF6Tqnz.cjs} +3 -1
  34. package/dist/{ld-BRqKkOSf.mjs → ld-D6UlzNnG.mjs} +3 -3
  35. package/dist/{metrics-Z0UkrR_1.mjs → metrics-62qolVUY.mjs} +1 -1
  36. package/dist/{middleware-C_kPmXE6.mjs → middleware-C3Q3CoR8.mjs} +1 -1
  37. package/dist/{middleware-RTcKhytV.mjs → middleware-CfbFdEO-.mjs} +96 -194
  38. package/dist/{middleware-DanzltB1.cjs → middleware-DkrXqZO_.cjs} +106 -34
  39. package/dist/{middleware-B-CIN962.js → middleware-xw9fBZoB.js} +106 -34
  40. package/dist/mod.cjs +4 -4
  41. package/dist/mod.js +4 -4
  42. package/dist/nodeinfo/client.test.mjs +2 -2
  43. package/dist/nodeinfo/handler.test.mjs +2 -2
  44. package/dist/nodeinfo/types.test.mjs +2 -2
  45. package/dist/otel/exporter.test.mjs +1 -1
  46. package/dist/{owner-B0rmuoQB.mjs → owner-DWnLg-vk.mjs} +2 -2
  47. package/dist/{proof-Bau45ZUZ.js → proof-CUSzBztF.js} +1 -1
  48. package/dist/{proof-CKczQTG0.cjs → proof-DF6CU-SU.cjs} +1 -1
  49. package/dist/{proof-B7mAFVed.mjs → proof-mf6h7n4_.mjs} +3 -3
  50. package/dist/{send-BFukz_kE.mjs → send-LJUzNpVF.mjs} +3 -3
  51. package/dist/sig/http.test.mjs +6 -6
  52. package/dist/sig/key.test.mjs +213 -7
  53. package/dist/sig/ld.test.mjs +5 -5
  54. package/dist/sig/mod.cjs +2 -2
  55. package/dist/sig/mod.js +2 -2
  56. package/dist/sig/owner.test.mjs +4 -4
  57. package/dist/sig/proof.test.mjs +5 -4
  58. package/dist/{std__assert-BvGuO4xO.mjs → std__assert-BjJn2daJ.mjs} +2 -2
  59. package/dist/{temporal-n6j0MNF9.mjs → temporal-_3yIqdvy.mjs} +1 -1
  60. package/dist/utils/docloader.test.mjs +114 -39
  61. package/dist/utils/kv-cache.test.mjs +1 -1
  62. package/dist/utils/mod.cjs +1 -1
  63. package/dist/utils/mod.js +1 -1
  64. package/package.json +7 -7
  65. /package/dist/{assert-jSdZMYam.mjs → assert-CEO06XCw.mjs} +0 -0
  66. /package/dist/{circuit-breaker-test-utils-Dm5MM6l9.mjs → circuit-breaker-test-utils-CAdwjgMi.mjs} +0 -0
  67. /package/dist/{negotiation-CDW-_gUU.mjs → negotiation-DDstyBvc.mjs} +0 -0
  68. /package/dist/{retry-_VvV0h9f.mjs → retry-CXg_MBI-.mjs} +0 -0
@@ -4,13 +4,13 @@ import { getLogger } from "@logtape/logtape";
4
4
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
5
5
  import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
6
6
  import { encodeHex } from "byte-encodings/hex";
7
- import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
7
+ import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
8
8
  import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
9
9
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
10
10
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
11
11
  //#region deno.json
12
12
  var name = "@fedify/fedify";
13
- var version = "2.3.12-dev.2365+89254121";
13
+ var version = "2.3.13";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -960,6 +960,65 @@ function getDurationMs(start) {
960
960
  }
961
961
  //#endregion
962
962
  //#region src/sig/key.ts
963
+ /** Classifies decoding failures without hiding unexpected loader errors. */
964
+ function classifyKeyJsonLdError(error, seen = /* @__PURE__ */ new Set()) {
965
+ if (!(error instanceof Error) || !error.name.startsWith("jsonld.")) return null;
966
+ if (seen.has(error)) throw error;
967
+ seen.add(error);
968
+ const details = error.details;
969
+ if (typeof details?.code !== "string") return null;
970
+ if (details.code !== "loading remote context failed" || !("cause" in details) && !("cause" in error)) return "invalid";
971
+ const cause = "cause" in details ? details.cause : error.cause;
972
+ const nestedFailure = classifyKeyJsonLdError(cause, seen);
973
+ if (nestedFailure != null) return nestedFailure;
974
+ if (cause instanceof SyntaxError) return "invalid";
975
+ if (cause instanceof FetchError || cause instanceof UrlError) return "fetch";
976
+ if (cause instanceof Error && [
977
+ "AbortError",
978
+ "TimeoutError",
979
+ "NetworkError"
980
+ ].includes(cause.name)) return "fetch";
981
+ if (cause instanceof TypeError) {
982
+ const urlCode = cause.code;
983
+ if (cause.name === "InvalidContextReferenceError") return "invalid";
984
+ if (urlCode === "ERR_INVALID_URL" || cause.message === "Invalid URL string." || /^Invalid URL(?::|$)/.test(cause.message) || / cannot be parsed as a URL\.?$/.test(cause.message)) return typeof details.url === "string" && /^[A-Za-z][A-Za-z0-9+.-]*:/.test(details.url) && !URL.canParse(details.url) ? "invalid" : "fetch";
985
+ const networkCause = cause.cause;
986
+ const code = cause.code ?? networkCause?.code;
987
+ if (typeof code === "string" && [
988
+ "ECONNREFUSED",
989
+ "ECONNRESET",
990
+ "ECONNABORTED",
991
+ "ETIMEDOUT",
992
+ "EHOSTUNREACH",
993
+ "ENETUNREACH",
994
+ "ENOTFOUND",
995
+ "EAI_AGAIN",
996
+ "UND_ERR_CONNECT_TIMEOUT",
997
+ "UND_ERR_HEADERS_TIMEOUT",
998
+ "UND_ERR_BODY_TIMEOUT",
999
+ "UND_ERR_SOCKET"
1000
+ ].includes(code) || /^(fetch failed|Failed to fetch|error sending request|error reading a body from connection|Unable to connect|ConnectionRefused|Network connection lost|The socket connection was closed unexpectedly)/.test(cause.message)) return "fetch";
1001
+ }
1002
+ throw cause;
1003
+ }
1004
+ /** Carries a context transport failure through the key decoding boundary. */
1005
+ var KeyContextFetchError = class extends Error {
1006
+ error;
1007
+ constructor(error) {
1008
+ super("Failed to fetch a key's JSON-LD context.", { cause: error });
1009
+ this.error = error;
1010
+ }
1011
+ };
1012
+ function contextFetchError(error) {
1013
+ const seen = /* @__PURE__ */ new Set();
1014
+ while (error instanceof Error && !seen.has(error)) {
1015
+ seen.add(error);
1016
+ const details = error.details;
1017
+ if (!error.name.startsWith("jsonld.") || details?.code !== "loading remote context failed") break;
1018
+ error = "cause" in details ? details.cause : error.cause;
1019
+ }
1020
+ return new KeyContextFetchError(error);
1021
+ }
963
1022
  /**
964
1023
  * Checks if the given key is valid and supported. No-op if the key is valid,
965
1024
  * otherwise throws an error.
@@ -1054,6 +1113,9 @@ async function importJwk(jwk, type) {
1054
1113
  * @internal
1055
1114
  */
1056
1115
  async function fetchActorDocument(actorId, options = {}) {
1116
+ return await fetchActorDocumentInternal(actorId, options, false);
1117
+ }
1118
+ async function fetchActorDocumentInternal(actorId, options, propagateContextFetchErrors) {
1057
1119
  const logger = getLogger([
1058
1120
  "fedify",
1059
1121
  "sig",
@@ -1084,9 +1146,12 @@ async function fetchActorDocument(actorId, options = {}) {
1084
1146
  baseUrl: documentUrl
1085
1147
  });
1086
1148
  } catch (error) {
1087
- if (!(error instanceof TypeError)) throw error;
1088
- logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
1149
+ const failure = classifyKeyJsonLdError(error);
1150
+ if (failure === "fetch" && propagateContextFetchErrors) throw contextFetchError(error);
1151
+ if (failure == null && !(error instanceof TypeError)) throw error;
1152
+ logger.debug("Failed to decode the actor document at {documentUrl}: {error}", {
1089
1153
  documentUrl: documentUrl.href,
1154
+ failure: failure ?? "invalid",
1090
1155
  error
1091
1156
  });
1092
1157
  return null;
@@ -1125,6 +1190,9 @@ async function fetchActorDocument(actorId, options = {}) {
1125
1190
  * @internal
1126
1191
  */
1127
1192
  async function verifyKeyOwnership(key, options = {}) {
1193
+ return await verifyKeyOwnershipInternal(key, options, false);
1194
+ }
1195
+ async function verifyKeyOwnershipInternal(key, options, propagateContextFetchErrors) {
1128
1196
  const logger = getLogger([
1129
1197
  "fedify",
1130
1198
  "sig",
@@ -1134,7 +1202,7 @@ async function verifyKeyOwnership(key, options = {}) {
1134
1202
  if (keyId == null) return null;
1135
1203
  const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1136
1204
  if (claimedOwnerId == null) return null;
1137
- const owner = await fetchActorDocument(claimedOwnerId, options);
1205
+ const owner = await fetchActorDocumentInternal(claimedOwnerId, options, propagateContextFetchErrors);
1138
1206
  if (owner == null) {
1139
1207
  logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
1140
1208
  keyId: keyId.href,
@@ -1286,6 +1354,21 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1286
1354
  tracerProvider
1287
1355
  });
1288
1356
  } catch (e) {
1357
+ const failure = classifyKeyJsonLdError(e);
1358
+ if (failure === "fetch") throw contextFetchError(e);
1359
+ if (failure != null) {
1360
+ logger.debug("Failed to decode key {keyId}: {error}", {
1361
+ keyId,
1362
+ failure,
1363
+ error: e
1364
+ });
1365
+ await keyCache?.set(cacheKey, null);
1366
+ await clearFetchErrorMetadata(cacheKey, keyCache);
1367
+ return {
1368
+ key: null,
1369
+ cached: false
1370
+ };
1371
+ }
1289
1372
  if (!(e instanceof TypeError)) throw e;
1290
1373
  try {
1291
1374
  object = await cls.fromJsonLd(document, {
@@ -1294,8 +1377,14 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1294
1377
  tracerProvider
1295
1378
  });
1296
1379
  } catch (e) {
1297
- if (e instanceof TypeError) {
1298
- logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
1380
+ const failure = classifyKeyJsonLdError(e);
1381
+ if (failure === "fetch") throw contextFetchError(e);
1382
+ if (failure != null || e instanceof TypeError) {
1383
+ logger.debug("Failed to decode key {keyId}: {error}", {
1384
+ keyId,
1385
+ failure: failure ?? "invalid",
1386
+ error: e
1387
+ });
1299
1388
  await keyCache?.set(cacheKey, null);
1300
1389
  await clearFetchErrorMetadata(cacheKey, keyCache);
1301
1390
  return {
@@ -1387,16 +1476,17 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1387
1476
  const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1388
1477
  if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
1389
1478
  else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
1390
- if (await verifyKeyOwnership(key, {
1479
+ if (await verifyKeyOwnershipInternal(key, {
1391
1480
  documentLoader,
1392
1481
  contextLoader,
1393
1482
  tracerProvider
1394
- }) == null) {
1483
+ }, true) == null) {
1395
1484
  logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
1396
1485
  keyId,
1397
1486
  claimedOwnerId: claimedOwnerId.href
1398
1487
  });
1399
1488
  await keyCache?.set(cacheKey, null);
1489
+ await clearFetchErrorMetadata(cacheKey, keyCache);
1400
1490
  return {
1401
1491
  key: null,
1402
1492
  cached: false
@@ -1447,9 +1537,17 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1447
1537
  outcome = classified;
1448
1538
  return errored;
1449
1539
  }
1450
- const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
1451
- outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1452
- return resolved;
1540
+ try {
1541
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
1542
+ outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1543
+ return resolved;
1544
+ } catch (error) {
1545
+ if (!(error instanceof KeyContextFetchError)) throw error;
1546
+ const classified = classifyFetchError(error.error);
1547
+ const errored = await onFetchError(error.error, cacheKey, keyId, keyCache, logger);
1548
+ outcome = classified;
1549
+ return errored;
1550
+ }
1453
1551
  } finally {
1454
1552
  recordKeyLookup(options.meterProvider, {
1455
1553
  durationMs: getDurationMs(start),
@@ -0,0 +1,180 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { n as version, t as name } from "./deno-XpcjbHwj.mjs";
5
+ import { g as recordInboxActivity, n as getDurationMs, r as getFederationMetrics } from "./metrics-62qolVUY.mjs";
6
+ import { getLogger } from "@logtape/logtape";
7
+ import { getTypeId } from "@fedify/vocab";
8
+ import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
9
+ //#region src/federation/inbox.ts
10
+ /**
11
+ * Checks whether an activity's ID and all of its actor IDs have the same
12
+ * non-opaque origin. Activities without an ID have no origin to authenticate.
13
+ *
14
+ * The caller is responsible for rejecting a missing primary actor ID before
15
+ * relying on this check.
16
+ * @internal
17
+ */
18
+ function actorsHaveSameOrigin(activity) {
19
+ if (activity.id == null) return true;
20
+ const origin = activity.id.origin;
21
+ return origin !== "null" && activity.actorIds.every((actor) => actor.origin === origin);
22
+ }
23
+ async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
24
+ const logger = getLogger([
25
+ "fedify",
26
+ "federation",
27
+ "inbox"
28
+ ]);
29
+ if (activity.actorId == null) {
30
+ logger.error("Missing actor.", { activity: json });
31
+ span.setStatus({
32
+ code: SpanStatusCode.ERROR,
33
+ message: "Missing actor."
34
+ });
35
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
36
+ return "missingActor";
37
+ }
38
+ let cacheKey = null;
39
+ if (activity.id != null) {
40
+ const inboxContext = inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href);
41
+ const strategy = idempotencyStrategy ?? "per-inbox";
42
+ let keyString;
43
+ if (typeof strategy === "function") keyString = await strategy(inboxContext, activity);
44
+ else switch (strategy) {
45
+ case "global":
46
+ keyString = activity.id.href;
47
+ break;
48
+ case "per-origin":
49
+ keyString = `${ctx.origin}\n${activity.id.href}`;
50
+ break;
51
+ case "per-inbox":
52
+ keyString = `${ctx.origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
53
+ break;
54
+ default: keyString = `${ctx.origin}\n${activity.id.href}`;
55
+ }
56
+ if (keyString != null) cacheKey = [...kvPrefixes.activityIdempotence, keyString];
57
+ }
58
+ if (cacheKey != null) {
59
+ if (await kv.get(cacheKey) === true) {
60
+ logger.debug("Activity {activityId} has already been processed.", {
61
+ activityId: activity.id?.href,
62
+ activity: json,
63
+ recipient
64
+ });
65
+ span.setStatus({
66
+ code: SpanStatusCode.UNSET,
67
+ message: `Activity ${activity.id?.href} has already been processed.`
68
+ });
69
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
70
+ return "alreadyProcessed";
71
+ }
72
+ }
73
+ span.setAttribute("activitypub.actor.id", activity.actorId.href);
74
+ if (queue != null) {
75
+ const carrier = {};
76
+ propagation.inject(context.active(), carrier);
77
+ try {
78
+ await queue.enqueue({
79
+ type: "inbox",
80
+ id: crypto.randomUUID(),
81
+ baseUrl: ctx.origin,
82
+ activity: originalJson ?? json,
83
+ ...normalizedActivity == null ? {} : { normalizedActivity },
84
+ ...ldSignatureVerified == null ? {} : { ldSignatureVerified },
85
+ identifier: recipient,
86
+ attempt: 0,
87
+ started: (/* @__PURE__ */ new Date()).toISOString(),
88
+ traceContext: carrier
89
+ });
90
+ } catch (error) {
91
+ logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
92
+ error,
93
+ activityId: activity.id?.href,
94
+ activity: json,
95
+ recipient
96
+ });
97
+ span.setStatus({
98
+ code: SpanStatusCode.ERROR,
99
+ message: `Failed to enqueue the incoming activity ${activity.id?.href}.`
100
+ });
101
+ throw error;
102
+ }
103
+ getFederationMetrics(meterProvider).recordQueueTaskEnqueued({
104
+ role: "inbox",
105
+ queue,
106
+ activityType: getTypeId(activity).href
107
+ }, 0);
108
+ recordInboxActivity(meterProvider, "queued", getTypeId(activity).href);
109
+ logger.info("Activity {activityId} is enqueued.", {
110
+ activityId: activity.id?.href,
111
+ activity: json,
112
+ recipient
113
+ });
114
+ return "enqueued";
115
+ }
116
+ tracerProvider = tracerProvider ?? trace.getTracerProvider();
117
+ return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
118
+ const dispatched = inboxListeners?.dispatchWithClass(activity);
119
+ if (dispatched == null) {
120
+ logger.error("Unsupported activity type:\n{activity}", {
121
+ activity: json,
122
+ recipient
123
+ });
124
+ span.setStatus({
125
+ code: SpanStatusCode.UNSET,
126
+ message: `Unsupported activity type: ${getTypeId(activity).href}`
127
+ });
128
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
129
+ span.end();
130
+ return "unsupportedActivity";
131
+ }
132
+ const { class: cls, listener } = dispatched;
133
+ span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
134
+ try {
135
+ const activityType = getTypeId(activity).href;
136
+ const started = performance.now();
137
+ try {
138
+ const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
139
+ await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
140
+ } finally {
141
+ getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
142
+ }
143
+ } catch (error) {
144
+ try {
145
+ await inboxErrorHandler?.(ctx, error);
146
+ } catch (error) {
147
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
148
+ error,
149
+ activityId: activity.id?.href,
150
+ activity: json,
151
+ recipient
152
+ });
153
+ }
154
+ logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
155
+ error,
156
+ activityId: activity.id?.href,
157
+ activity: json,
158
+ recipient
159
+ });
160
+ span.setStatus({
161
+ code: SpanStatusCode.ERROR,
162
+ message: String(error)
163
+ });
164
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
165
+ span.end();
166
+ return "error";
167
+ }
168
+ recordInboxActivity(meterProvider, "processed", getTypeId(activity).href);
169
+ if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
170
+ logger.info("Activity {activityId} has been processed.", {
171
+ activityId: activity.id?.href,
172
+ activity: json,
173
+ recipient
174
+ });
175
+ span.end();
176
+ return "success";
177
+ });
178
+ }
179
+ //#endregion
180
+ export { routeActivity as n, actorsHaveSameOrigin as t };
@@ -1,13 +1,72 @@
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-DSS6EkPl.mjs";
5
- import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-Z0UkrR_1.mjs";
4
+ import { n as version, t as name } from "./deno-XpcjbHwj.mjs";
5
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-62qolVUY.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
9
- import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
9
+ import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
10
10
  //#region src/sig/key.ts
11
+ /** Classifies decoding failures without hiding unexpected loader errors. */
12
+ function classifyKeyJsonLdError(error, seen = /* @__PURE__ */ new Set()) {
13
+ if (!(error instanceof Error) || !error.name.startsWith("jsonld.")) return null;
14
+ if (seen.has(error)) throw error;
15
+ seen.add(error);
16
+ const details = error.details;
17
+ if (typeof details?.code !== "string") return null;
18
+ if (details.code !== "loading remote context failed" || !("cause" in details) && !("cause" in error)) return "invalid";
19
+ const cause = "cause" in details ? details.cause : error.cause;
20
+ const nestedFailure = classifyKeyJsonLdError(cause, seen);
21
+ if (nestedFailure != null) return nestedFailure;
22
+ if (cause instanceof SyntaxError) return "invalid";
23
+ if (cause instanceof FetchError || cause instanceof UrlError) return "fetch";
24
+ if (cause instanceof Error && [
25
+ "AbortError",
26
+ "TimeoutError",
27
+ "NetworkError"
28
+ ].includes(cause.name)) return "fetch";
29
+ if (cause instanceof TypeError) {
30
+ const urlCode = cause.code;
31
+ if (cause.name === "InvalidContextReferenceError") return "invalid";
32
+ if (urlCode === "ERR_INVALID_URL" || cause.message === "Invalid URL string." || /^Invalid URL(?::|$)/.test(cause.message) || / cannot be parsed as a URL\.?$/.test(cause.message)) return typeof details.url === "string" && /^[A-Za-z][A-Za-z0-9+.-]*:/.test(details.url) && !URL.canParse(details.url) ? "invalid" : "fetch";
33
+ const networkCause = cause.cause;
34
+ const code = cause.code ?? networkCause?.code;
35
+ if (typeof code === "string" && [
36
+ "ECONNREFUSED",
37
+ "ECONNRESET",
38
+ "ECONNABORTED",
39
+ "ETIMEDOUT",
40
+ "EHOSTUNREACH",
41
+ "ENETUNREACH",
42
+ "ENOTFOUND",
43
+ "EAI_AGAIN",
44
+ "UND_ERR_CONNECT_TIMEOUT",
45
+ "UND_ERR_HEADERS_TIMEOUT",
46
+ "UND_ERR_BODY_TIMEOUT",
47
+ "UND_ERR_SOCKET"
48
+ ].includes(code) || /^(fetch failed|Failed to fetch|error sending request|error reading a body from connection|Unable to connect|ConnectionRefused|Network connection lost|The socket connection was closed unexpectedly)/.test(cause.message)) return "fetch";
49
+ }
50
+ throw cause;
51
+ }
52
+ /** Carries a context transport failure through the key decoding boundary. */
53
+ var KeyContextFetchError = class extends Error {
54
+ error;
55
+ constructor(error) {
56
+ super("Failed to fetch a key's JSON-LD context.", { cause: error });
57
+ this.error = error;
58
+ }
59
+ };
60
+ function contextFetchError(error) {
61
+ const seen = /* @__PURE__ */ new Set();
62
+ while (error instanceof Error && !seen.has(error)) {
63
+ seen.add(error);
64
+ const details = error.details;
65
+ if (!error.name.startsWith("jsonld.") || details?.code !== "loading remote context failed") break;
66
+ error = "cause" in details ? details.cause : error.cause;
67
+ }
68
+ return new KeyContextFetchError(error);
69
+ }
11
70
  /**
12
71
  * Checks if the given key is valid and supported. No-op if the key is valid,
13
72
  * otherwise throws an error.
@@ -102,6 +161,9 @@ async function importJwk(jwk, type) {
102
161
  * @internal
103
162
  */
104
163
  async function fetchActorDocument(actorId, options = {}) {
164
+ return await fetchActorDocumentInternal(actorId, options, false);
165
+ }
166
+ async function fetchActorDocumentInternal(actorId, options, propagateContextFetchErrors) {
105
167
  const logger = getLogger([
106
168
  "fedify",
107
169
  "sig",
@@ -132,9 +194,12 @@ async function fetchActorDocument(actorId, options = {}) {
132
194
  baseUrl: documentUrl
133
195
  });
134
196
  } catch (error) {
135
- if (!(error instanceof TypeError)) throw error;
136
- logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
197
+ const failure = classifyKeyJsonLdError(error);
198
+ if (failure === "fetch" && propagateContextFetchErrors) throw contextFetchError(error);
199
+ if (failure == null && !(error instanceof TypeError)) throw error;
200
+ logger.debug("Failed to decode the actor document at {documentUrl}: {error}", {
137
201
  documentUrl: documentUrl.href,
202
+ failure: failure ?? "invalid",
138
203
  error
139
204
  });
140
205
  return null;
@@ -173,6 +238,9 @@ async function fetchActorDocument(actorId, options = {}) {
173
238
  * @internal
174
239
  */
175
240
  async function verifyKeyOwnership(key, options = {}) {
241
+ return await verifyKeyOwnershipInternal(key, options, false);
242
+ }
243
+ async function verifyKeyOwnershipInternal(key, options, propagateContextFetchErrors) {
176
244
  const logger = getLogger([
177
245
  "fedify",
178
246
  "sig",
@@ -182,7 +250,7 @@ async function verifyKeyOwnership(key, options = {}) {
182
250
  if (keyId == null) return null;
183
251
  const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
184
252
  if (claimedOwnerId == null) return null;
185
- const owner = await fetchActorDocument(claimedOwnerId, options);
253
+ const owner = await fetchActorDocumentInternal(claimedOwnerId, options, propagateContextFetchErrors);
186
254
  if (owner == null) {
187
255
  logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
188
256
  keyId: keyId.href,
@@ -334,6 +402,21 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
334
402
  tracerProvider
335
403
  });
336
404
  } catch (e) {
405
+ const failure = classifyKeyJsonLdError(e);
406
+ if (failure === "fetch") throw contextFetchError(e);
407
+ if (failure != null) {
408
+ logger.debug("Failed to decode key {keyId}: {error}", {
409
+ keyId,
410
+ failure,
411
+ error: e
412
+ });
413
+ await keyCache?.set(cacheKey, null);
414
+ await clearFetchErrorMetadata(cacheKey, keyCache);
415
+ return {
416
+ key: null,
417
+ cached: false
418
+ };
419
+ }
337
420
  if (!(e instanceof TypeError)) throw e;
338
421
  try {
339
422
  object = await cls.fromJsonLd(document, {
@@ -342,8 +425,14 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
342
425
  tracerProvider
343
426
  });
344
427
  } catch (e) {
345
- if (e instanceof TypeError) {
346
- logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
428
+ const failure = classifyKeyJsonLdError(e);
429
+ if (failure === "fetch") throw contextFetchError(e);
430
+ if (failure != null || e instanceof TypeError) {
431
+ logger.debug("Failed to decode key {keyId}: {error}", {
432
+ keyId,
433
+ failure: failure ?? "invalid",
434
+ error: e
435
+ });
347
436
  await keyCache?.set(cacheKey, null);
348
437
  await clearFetchErrorMetadata(cacheKey, keyCache);
349
438
  return {
@@ -435,16 +524,17 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
435
524
  const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
436
525
  if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
437
526
  else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
438
- if (await verifyKeyOwnership(key, {
527
+ if (await verifyKeyOwnershipInternal(key, {
439
528
  documentLoader,
440
529
  contextLoader,
441
530
  tracerProvider
442
- }) == null) {
531
+ }, true) == null) {
443
532
  logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
444
533
  keyId,
445
534
  claimedOwnerId: claimedOwnerId.href
446
535
  });
447
536
  await keyCache?.set(cacheKey, null);
537
+ await clearFetchErrorMetadata(cacheKey, keyCache);
448
538
  return {
449
539
  key: null,
450
540
  cached: false
@@ -495,9 +585,17 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
495
585
  outcome = classified;
496
586
  return errored;
497
587
  }
498
- const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
499
- outcome = { result: resolved.key != null ? "fetched" : "invalid" };
500
- return resolved;
588
+ try {
589
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
590
+ outcome = { result: resolved.key != null ? "fetched" : "invalid" };
591
+ return resolved;
592
+ } catch (error) {
593
+ if (!(error instanceof KeyContextFetchError)) throw error;
594
+ const classified = classifyFetchError(error.error);
595
+ const errored = await onFetchError(error.error, cacheKey, keyId, keyCache, logger);
596
+ outcome = classified;
597
+ return errored;
598
+ }
501
599
  } finally {
502
600
  recordKeyLookup(options.meterProvider, {
503
601
  durationMs: getDurationMs(start),
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { T as recordDocumentCache, f as validateCryptoKey, t as doubleKnock } from "./http-LMsqGo8j.js";
3
+ import { T as recordDocumentCache, f as validateCryptoKey, t as doubleKnock } from "./http-GHVGCeb5.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
6
6
  import { curry } from "es-toolkit";
@@ -57,6 +57,8 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
57
57
  });
58
58
  }
59
59
  async function validateUrl(url) {
60
+ const protocol = new URL(url).protocol;
61
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
60
62
  if (!allowPrivateAddress) try {
61
63
  await validatePublicUrl(url);
62
64
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-Z0UkrR_1.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-62qolVUY.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,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-DdaLQHim.cjs");
4
+ const require_http = require("./http-DwPnG87d.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
7
7
  let es_toolkit = require("es-toolkit");
@@ -58,6 +58,8 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
58
58
  });
59
59
  }
60
60
  async function validateUrl(url) {
61
+ const protocol = new URL(url).protocol;
62
+ if (protocol !== "http:" && protocol !== "https:") throw new _fedify_vocab_runtime.UrlError(`Unsupported protocol: ${protocol}`);
61
63
  if (!allowPrivateAddress) try {
62
64
  await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
63
65
  } catch (error) {
@@ -1,9 +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-DSS6EkPl.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-Z0UkrR_1.mjs";
6
- import { r as fetchKey, s as validateCryptoKey } from "./key-vnmAGZ3_.mjs";
4
+ import { n as version, t as name } from "./deno-XpcjbHwj.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-62qolVUY.mjs";
6
+ import { r as fetchKey, s as validateCryptoKey } from "./key-PdEu1lDF.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
9
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
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-DSS6EkPl.mjs";
4
+ import { n as version, t as name } from "./deno-XpcjbHwj.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { FetchError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/metrics.ts
@@ -1,5 +1,5 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as FederationImpl } from "./middleware-RTcKhytV.mjs";
4
+ import { n as FederationImpl } from "./middleware-CfbFdEO-.mjs";
5
5
  export { FederationImpl };