@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2492

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 (141) hide show
  1. package/README.md +3 -0
  2. package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
  3. package/dist/{builder-Da8b9O6v.mjs → builder-DphK9v7f.mjs} +5 -5
  4. package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.cts} +0 -1
  5. package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.ts} +0 -1
  6. package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
  7. package/dist/compat/mod.d.cts +1 -1
  8. package/dist/compat/mod.d.ts +1 -1
  9. package/dist/compat/transformers.test.mjs +1 -1
  10. package/dist/{compound-proof-D1djO5fa.mjs → compound-proof-cYtvlTgF.mjs} +2 -2
  11. package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
  12. package/dist/{context-BdB54V-A.d.ts → context-DQJE9YfH.d.ts} +332 -6
  13. package/dist/{context-CQ9H8vNW.d.cts → context-DSuL3YWx.d.cts} +332 -6
  14. package/dist/{deno-eNpfj4oR.mjs → deno-iOAA7iRN.mjs} +1 -1
  15. package/dist/docloader-DCZkiTeF.mjs +136 -0
  16. package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
  17. package/dist/federation/builder.test.mjs +1 -1
  18. package/dist/federation/compound-fanout.test.mjs +6 -3
  19. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  20. package/dist/federation/handler.test.mjs +22 -7
  21. package/dist/federation/hashlink-media.test.mjs +2 -2
  22. package/dist/federation/idempotency.test.mjs +2 -2
  23. package/dist/federation/inbox-actorless.test.d.mts +2 -0
  24. package/dist/federation/inbox-actorless.test.mjs +487 -0
  25. package/dist/federation/inbox-authentication.test.d.mts +2 -0
  26. package/dist/federation/inbox-authentication.test.mjs +116 -0
  27. package/dist/federation/inbox-report.test.mjs +5 -5
  28. package/dist/federation/inbox-view.test.d.mts +2 -0
  29. package/dist/federation/inbox-view.test.mjs +369 -0
  30. package/dist/federation/inbox.test.mjs +22 -12
  31. package/dist/federation/metrics.test.mjs +1 -1
  32. package/dist/federation/middleware.test.mjs +470 -35
  33. package/dist/federation/mod.cjs +2 -1
  34. package/dist/federation/mod.d.cts +4 -4
  35. package/dist/federation/mod.d.ts +4 -4
  36. package/dist/federation/mod.js +8 -2
  37. package/dist/federation/mq.test.mjs +36 -1
  38. package/dist/federation/outgoing-proof.test.mjs +9 -6
  39. package/dist/federation/portable-actor.test.mjs +3 -3
  40. package/dist/federation/portable-chain.test.mjs +3 -3
  41. package/dist/federation/portable-collection.test.mjs +4 -4
  42. package/dist/federation/portable-delivery.test.mjs +2 -2
  43. package/dist/federation/portable-gateway-keys.test.mjs +13 -7
  44. package/dist/federation/portable-inbox.test.mjs +4 -4
  45. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  46. package/dist/federation/portable-route-activity.test.mjs +2 -2
  47. package/dist/federation/portable.test.mjs +3 -3
  48. package/dist/federation/send.test.mjs +502 -93
  49. package/dist/federation/tasks/enqueue.test.mjs +3 -3
  50. package/dist/federation/tasks/tasks.test.mjs +4 -4
  51. package/dist/federation/temporal.test.mjs +1 -1
  52. package/dist/federation/webfinger.test.mjs +2 -2
  53. package/dist/{federation-BVH7drJA.cjs → federation-D0AZw_aw.cjs} +1415 -236
  54. package/dist/{http-BUJ5SFh6.mjs → http-BoJCMnjY.mjs} +204 -106
  55. package/dist/{http-DlNxxW_5.js → http-ChXUU_3c.js} +319 -123
  56. package/dist/{http-BfW3wyQ4.cjs → http-kv1LZw-r.cjs} +339 -125
  57. package/dist/inbox-BjihTKLo.mjs +312 -0
  58. package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
  59. package/dist/inbox-view-D-skypg-.mjs +385 -0
  60. package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
  61. package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
  62. package/dist/{kv-cache-D_m3A241.mjs → kv-cache-Cmsyke4d.mjs} +1 -1
  63. package/dist/{kv-cache-7015lbrx.js → kv-cache-DQcte7ES.js} +73 -28
  64. package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-Ddh5sMxH.cjs} +72 -27
  65. package/dist/ld-CWVpo_NM.d.cts +250 -0
  66. package/dist/{ld-CD2CsPxq.mjs → ld-CdvcOEXQ.mjs} +52 -9
  67. package/dist/ld-Inagww4R.d.ts +250 -0
  68. package/dist/{metrics-D3LHk6Z4.mjs → metrics-CpxvUrBz.mjs} +1 -1
  69. package/dist/{middleware-N9eM16XE.js → middleware-BHb5toqA.js} +1413 -247
  70. package/dist/{middleware-CCDXvXOG.mjs → middleware-BKs2DLUF.mjs} +1 -1
  71. package/dist/{middleware-CzoWvrdZ.mjs → middleware-CoBLrTKX.mjs} +533 -132
  72. package/dist/{mod-B0dGgQsg.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
  73. package/dist/mod-B4kBw1-8.d.ts +154 -0
  74. package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
  75. package/dist/mod-C1nT74IG.d.cts +154 -0
  76. package/dist/{mod-D0bxB8jS.d.ts → mod-CIeBgsOC.d.ts} +1 -1
  77. package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
  78. package/dist/mod.cjs +6 -5
  79. package/dist/mod.d.cts +9 -9
  80. package/dist/mod.d.ts +9 -9
  81. package/dist/mod.js +6 -6
  82. package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
  83. package/dist/nodeinfo/client.test.mjs +337 -19
  84. package/dist/nodeinfo/handler.test.mjs +2 -2
  85. package/dist/nodeinfo/mod.cjs +1 -1
  86. package/dist/nodeinfo/mod.d.cts +1 -1
  87. package/dist/nodeinfo/mod.d.ts +1 -1
  88. package/dist/nodeinfo/mod.js +1 -1
  89. package/dist/otel/exporter.test.mjs +257 -1
  90. package/dist/otel/mod.cjs +36 -30
  91. package/dist/otel/mod.d.cts +13 -0
  92. package/dist/otel/mod.d.ts +13 -0
  93. package/dist/otel/mod.js +36 -30
  94. package/dist/{owner-C5-FVX6t.mjs → owner-DTnVpUKA.mjs} +3 -3
  95. package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CJeYTL61.mjs} +2 -2
  96. package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-DciwBIia.cjs} +1 -1
  97. package/dist/{portable-collection-CC2zNAje.js → portable-collection-TmtpzD4E.js} +1 -1
  98. package/dist/{portable-key-DNc_flH-.mjs → portable-key-DcYok4ki.mjs} +1 -1
  99. package/dist/{proof-Cryb1c70.mjs → proof-GH7Poos_.mjs} +98 -10
  100. package/dist/{send-AF25pWaJ.mjs → send-BgGJh6pv.mjs} +174 -76
  101. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  102. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  103. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  104. package/dist/sig/compound-proof.test.mjs +2 -2
  105. package/dist/sig/http.test.mjs +553 -2
  106. package/dist/sig/key.test.mjs +1 -1
  107. package/dist/sig/ld.test.mjs +1 -1
  108. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  109. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  110. package/dist/sig/mod.cjs +2 -2
  111. package/dist/sig/mod.d.cts +3 -3
  112. package/dist/sig/mod.d.ts +3 -3
  113. package/dist/sig/mod.js +2 -2
  114. package/dist/sig/owner.test.mjs +1 -1
  115. package/dist/sig/portable-collection.test.mjs +1 -1
  116. package/dist/sig/portable-interop.test.mjs +3 -3
  117. package/dist/sig/portable-key.test.mjs +5 -5
  118. package/dist/sig/proof.test.mjs +45 -5
  119. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  120. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  121. package/dist/sig/verification.test.mjs +3 -3
  122. package/dist/{temporal-BsAeuUJj.mjs → temporal-gJINzcL4.mjs} +1 -1
  123. package/dist/testing/kv.test.mjs +1 -1
  124. package/dist/testing/mod.d.mts +288 -2
  125. package/dist/testing/mod.mjs +2 -2
  126. package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
  127. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  128. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  129. package/dist/utils/docloader.test.mjs +344 -36
  130. package/dist/utils/kv-cache.test.mjs +1 -1
  131. package/dist/utils/mod.cjs +1 -1
  132. package/dist/utils/mod.d.cts +1 -1
  133. package/dist/utils/mod.d.ts +1 -1
  134. package/dist/utils/mod.js +1 -1
  135. package/package.json +7 -7
  136. package/dist/docloader-B-0nqOB1.mjs +0 -91
  137. package/dist/inbox-CHvMaIFX.mjs +0 -188
  138. package/dist/mod-BRpER-B0.d.ts +0 -320
  139. package/dist/mod-xAfaLS4T.d.cts +0 -320
  140. package/dist/owner-BF0L9cMt.d.cts +0 -78
  141. package/dist/owner-BSzr5-I8.d.ts +0 -78
@@ -1,14 +1,43 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-D-XP_Ccb.cjs";
3
- import { _ as InboxVerificationKey, d as InboxProofPolicyFailureReason, h as InboxVerificationAttempt, w as VerifyRequestFailureReason, y as HttpMessageSignaturesSpec } from "./key-nYGY4QB9.cjs";
4
- import { n as GetKeyOwnerOptions } from "./owner-BF0L9cMt.cjs";
2
+ import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-3InDZbba.cjs";
3
+ import { _ as InboxVerificationKey, d as InboxProofPolicyFailureReason, h as InboxVerificationAttempt, w as VerifyRequestFailureReason, y as HttpMessageSignaturesSpec } from "./key-DlHT4R6k.cjs";
4
+ import { m as GetKeyOwnerOptions, n as EffectiveContextLoaderPolicy } from "./ld-CWVpo_NM.cjs";
5
5
  import { n as KvStore, t as KvKey } from "./kv-B2mzK5tl.cjs";
6
6
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, Tombstone, TraverseCollectionOptions } from "@fedify/vocab";
7
- import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions, PortableObjectVerifier } from "@fedify/vocab-runtime";
7
+ import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions, PortableObjectVerifier, RemoteDocument } from "@fedify/vocab-runtime";
8
8
  import { Link as Link$1, LookupWebFingerOptions, ResourceDescriptor } from "@fedify/webfinger";
9
9
  import { MeterProvider, Span, Tracer, TracerProvider } from "@opentelemetry/api";
10
10
  import { StandardSchemaV1 } from "@standard-schema/spec";
11
11
 
12
+ //#region src/federation/activity-listener.d.ts
13
+ /**
14
+ * Options for an inbox listener.
15
+ * @since 2.5.0
16
+ */
17
+ interface InboxListenerOptions {
18
+ /**
19
+ * Whether activities that declare no `actor` at all may reach the listener.
20
+ *
21
+ * Some protocols identify the requester without an `actor`; for example,
22
+ * Mastodon sends [FEP-7aa9] `FeatureRequest` activities without one. When
23
+ * this is `true`, such an activity is dispatched to the listener if Fedify
24
+ * authenticated at least one principal for it, i.e., the signer of its
25
+ * Object Integrity Proofs or Linked Data Signature, or otherwise the signer
26
+ * of the HTTP request, and its `id`, if any, has the same origin as that
27
+ * principal. The listener is then responsible for authorizing the request
28
+ * with {@link InboxContext.authentication}, e.g., with
29
+ * `featureInteraction.verifyRequest()` from
30
+ * `@fedify/interaction-controls`.
31
+ *
32
+ * An activity that declares an `actor` is never affected by this option;
33
+ * its actor must be authenticated as usual.
34
+ *
35
+ * [FEP-7aa9]: https://w3id.org/fep/7aa9
36
+ * @default `false`
37
+ */
38
+ readonly allowActorless?: boolean;
39
+ }
40
+ //#endregion
12
41
  //#region src/federation/inbox-report.d.ts
13
42
  /**
14
43
  * A JSON value retained from the incoming request.
@@ -51,6 +80,13 @@ type InboxAuthentication = {
51
80
  readonly type: "activityOriginMismatch";
52
81
  readonly activityId: URL;
53
82
  readonly actorIds: readonly URL[];
83
+ } | {
84
+ /**
85
+ * The activity declares no actor, and no principal was authenticated
86
+ * for it, although its listener allows activities without an actor.
87
+ * @since 2.5.0
88
+ */
89
+ readonly type: "noAuthenticatedPrincipal";
54
90
  };
55
91
  } | {
56
92
  readonly status: "skipped";
@@ -949,6 +985,18 @@ interface MessageQueue {
949
985
  */
950
986
  readonly atomicEnqueueMany?: boolean;
951
987
  /**
988
+ * The largest message, in bytes, that the backend can hold. Fedify compares
989
+ * it with a conservative estimate of a message's serialized size: the
990
+ * length of its JSON serialization encoded in UTF-8, plus 8 bytes for each
991
+ * JSON value in it, plus what encoding strings beyond Latin-1 as UTF-16
992
+ * adds, which also bounds binary serialization formats. When an
993
+ * incoming activity would make a larger inbox message, Fedify processes the
994
+ * activity right away instead of enqueueing it, as if there were no inbox
995
+ * queue. When omitted, messages are enqueued regardless of their size.
996
+ * @since 2.5.0
997
+ */
998
+ readonly maxMessageSize?: number;
999
+ /**
952
1000
  * Enqueues a message in the queue.
953
1001
  * @param message The message to enqueue.
954
1002
  * @param options Additional options for enqueuing the message.
@@ -1074,6 +1122,11 @@ declare class ParallelMessageQueue implements MessageQueue {
1074
1122
  * @since 2.4.0
1075
1123
  */
1076
1124
  readonly atomicEnqueueMany?: boolean;
1125
+ /**
1126
+ * Inherits the message size limit from the wrapped queue.
1127
+ * @since 2.5.0
1128
+ */
1129
+ readonly maxMessageSize?: number;
1077
1130
  readonly getDepth?: () => Promise<MessageQueueDepth>;
1078
1131
  /**
1079
1132
  * Constructs a new {@link ParallelMessageQueue} with the given queue and
@@ -1090,6 +1143,211 @@ declare class ParallelMessageQueue implements MessageQueue {
1090
1143
  listen(handler: (message: any) => Promise<void> | void, options?: MessageQueueListenOptions): Promise<void>;
1091
1144
  }
1092
1145
  //#endregion
1146
+ //#region src/federation/inbox-authentication.d.ts
1147
+ /**
1148
+ * A party that Fedify authenticated for an incoming activity, independently
1149
+ * of the activity's declared `actor`.
1150
+ * @since 2.5.0
1151
+ */
1152
+ interface InboxPrincipal {
1153
+ /**
1154
+ * What {@link InboxPrincipal.id} is:
1155
+ *
1156
+ * - `"actor"`: an actor whose ownership of the verifying key Fedify
1157
+ * confirmed with the actor's own document.
1158
+ * - `"portableAuthority"`: a DID whose verification method made a valid
1159
+ * Object Integrity Proof. It authenticates the [FEP-ef61] portable
1160
+ * actors of that DID, and nothing else.
1161
+ *
1162
+ * [FEP-ef61]: https://w3id.org/fep/ef61
1163
+ */
1164
+ readonly kind: "actor" | "portableAuthority";
1165
+ /** The actor ID, or the DID if {@link kind} is `"portableAuthority"`. */
1166
+ readonly id: URL;
1167
+ /**
1168
+ * How the principal was authenticated:
1169
+ *
1170
+ * - `"httpSignatures"`: it signed the HTTP request.
1171
+ * - `"linkedDataSignatures"`: it made the Linked Data Signature of the
1172
+ * activity.
1173
+ * - `"objectIntegrityProofs"`: it made an Object Integrity Proof of the
1174
+ * activity.
1175
+ * - `"origin"`: the activity was fetched again from the actor's own web
1176
+ * origin, which happens only for `Context.routeActivity()`.
1177
+ */
1178
+ readonly mechanism: "httpSignatures" | "linkedDataSignatures" | "objectIntegrityProofs" | "origin";
1179
+ /** The key or verification method that authenticated the principal. */
1180
+ readonly keyId: URL | null;
1181
+ }
1182
+ /**
1183
+ * Who Fedify authenticated for an incoming activity, and the interpretation of
1184
+ * the activity it authenticated. Inbox listeners receive it as
1185
+ * {@link InboxContext.authentication}.
1186
+ *
1187
+ * Authenticated principals are independent of the activity's declared
1188
+ * `actor`: an activity forwarded by a relay is signed by the relay in its
1189
+ * HTTP request, but its document may be signed by its author, and some
1190
+ * activities, such as [FEP-7aa9] `FeatureRequest`s sent by Mastodon, declare
1191
+ * no `actor` at all. A valid signature alone does not authorize anything;
1192
+ * use {@link isActorAuthenticated} to check whether a specific actor was
1193
+ * authenticated.
1194
+ *
1195
+ * [FEP-7aa9]: https://w3id.org/fep/7aa9
1196
+ * @since 2.5.0
1197
+ */
1198
+ interface InboxAuthenticationEvidence {
1199
+ /**
1200
+ * Whether signatures were verified at all. It is `false` only when
1201
+ * signature verification is skipped, or when there is no evidence, e.g.,
1202
+ * for an activity queued by an older version of Fedify. In that case all
1203
+ * the principal lists are empty.
1204
+ */
1205
+ readonly verified: boolean;
1206
+ /**
1207
+ * The principals that signed the HTTP request, i.e., the server that
1208
+ * delivered the activity, which may have forwarded it. It is empty when
1209
+ * the activity document was authenticated by its own signatures, so that
1210
+ * HTTP Signatures did not need to be verified.
1211
+ */
1212
+ readonly requestSigners: readonly InboxPrincipal[];
1213
+ /**
1214
+ * The principals that made the verified Linked Data Signature or Object
1215
+ * Integrity Proofs of the activity document. All of them are listed when
1216
+ * there are several proofs.
1217
+ */
1218
+ readonly documentSigners: readonly InboxPrincipal[];
1219
+ /**
1220
+ * The actors authenticated because `Context.routeActivity()` fetched the
1221
+ * activity again from their own web origin. It is always empty for
1222
+ * activities delivered to an inbox.
1223
+ */
1224
+ readonly originAuthenticated: readonly InboxPrincipal[];
1225
+ /**
1226
+ * The activity as Fedify interpreted it while authenticating it, in
1227
+ * expanded JSON-LD. Typed vocabulary objects omit some values, such as
1228
+ * blank node references and values they cannot decode, so authorization
1229
+ * that has to count every value of a property, e.g., that a property has
1230
+ * exactly one value, should inspect this instead. It is deeply frozen.
1231
+ * It is empty when there is no evidence.
1232
+ */
1233
+ readonly expandedActivity: readonly Record<string, unknown>[];
1234
+ /**
1235
+ * The base URL the activity was decoded with, e.g., the URL an activity
1236
+ * fetched by `Context.routeActivity()` was finally retrieved from, or
1237
+ * `null` if none was given. Relative IRIs left in
1238
+ * {@link expandedActivity} resolve against it, or against the activity's
1239
+ * own `@id` if it is `null`, as the decoder resolves them.
1240
+ */
1241
+ readonly baseUrl: URL | null;
1242
+ }
1243
+ /**
1244
+ * Options for {@link isActorAuthenticated}.
1245
+ * @since 2.5.0
1246
+ */
1247
+ interface IsActorAuthenticatedOptions {
1248
+ /**
1249
+ * Which principals to consider:
1250
+ *
1251
+ * - `"effective"` (default): the document signers if there are any,
1252
+ * otherwise the request signers if there are any, otherwise the actors
1253
+ * authenticated by their origin.
1254
+ * - `"document"`: the document signers only.
1255
+ */
1256
+ readonly scope?: "effective" | "document";
1257
+ }
1258
+ /**
1259
+ * Checks whether the given evidence authenticates the given actor.
1260
+ *
1261
+ * Actor IDs are compared exactly, except that [FEP-ef61] portable IDs and
1262
+ * compatible identifiers are compared by their canonical portable IDs, and
1263
+ * a portable authority (a DID) authenticates the portable actors of that DID.
1264
+ *
1265
+ * @example
1266
+ * ``` typescript
1267
+ * federation
1268
+ * .setInboxListeners("/users/{identifier}/inbox", "/inbox")
1269
+ * .on(Follow, async (ctx, follow) => {
1270
+ * if (follow.actorId == null) return;
1271
+ * // The actor is always authenticated for activities that declare one,
1272
+ * // but the same check works for any actor an activity names:
1273
+ * isActorAuthenticated(ctx.authentication, follow.actorId);
1274
+ * });
1275
+ * ```
1276
+ *
1277
+ * [FEP-ef61]: https://w3id.org/fep/ef61
1278
+ * @param evidence The authentication evidence, usually
1279
+ * {@link InboxContext.authentication}.
1280
+ * @param actorId The actor ID to check.
1281
+ * @param options Options.
1282
+ * @returns `true` if the actor is authenticated.
1283
+ * @since 2.5.0
1284
+ */
1285
+ declare function isActorAuthenticated(evidence: InboxAuthenticationEvidence, actorId: URL, options?: IsActorAuthenticatedOptions): boolean;
1286
+ /**
1287
+ * A {@link InboxPrincipal} as stored in a queued message.
1288
+ * @internal
1289
+ */
1290
+ interface SerializedInboxPrincipal {
1291
+ readonly kind: InboxPrincipal["kind"];
1292
+ readonly id: string;
1293
+ readonly mechanism: InboxPrincipal["mechanism"];
1294
+ readonly keyId: string | null;
1295
+ }
1296
+ /**
1297
+ * The principals of an {@link InboxAuthenticationEvidence} as stored in
1298
+ * a queued message. The expanded activity and the base URL are restored
1299
+ * from the queued verified view instead.
1300
+ * @internal
1301
+ */
1302
+ interface SerializedInboxAuthentication {
1303
+ readonly verified: boolean;
1304
+ readonly requestSigners: readonly SerializedInboxPrincipal[];
1305
+ readonly documentSigners: readonly SerializedInboxPrincipal[];
1306
+ readonly originAuthenticated: readonly SerializedInboxPrincipal[];
1307
+ }
1308
+ //#endregion
1309
+ //#region src/federation/inbox-view.d.ts
1310
+ /**
1311
+ * A {@link ContextSnapshotEntry} as stored in a queued message. A response
1312
+ * that equals the one Fedify bundles for the URL is stored as a reference to
1313
+ * the bundled context, along with its hash for diagnostics.
1314
+ * @internal
1315
+ */
1316
+ type SerializedContextSnapshotEntry = {
1317
+ readonly policy: EffectiveContextLoaderPolicy;
1318
+ readonly url: string; /** The SHA-256 of the canonical JSON of the whole response. */
1319
+ readonly sha256: string;
1320
+ } & ({
1321
+ readonly bundled: true;
1322
+ } | {
1323
+ readonly response: RemoteDocument;
1324
+ });
1325
+ /**
1326
+ * The interpretation of an incoming activity that Fedify authenticated, as
1327
+ * stored in a queued message. The document itself is not copied; it is
1328
+ * a field of the message.
1329
+ * @internal
1330
+ */
1331
+ interface SerializedVerifiedActivityView {
1332
+ /** The format version. */
1333
+ readonly version: 1;
1334
+ /** The context loader policy the activity was parsed with. */
1335
+ readonly policy: EffectiveContextLoaderPolicy;
1336
+ /**
1337
+ * Which document the activity was parsed from, with its Linked Data
1338
+ * Signature detached: the `activity` of the message, i.e., the payload as
1339
+ * received, or its `normalizedActivity`, i.e., the payload compacted for
1340
+ * Linked Data Signatures.
1341
+ */
1342
+ readonly source: "activity" | "normalizedActivity";
1343
+ /** The `baseUrl` option the activity was parsed with. */
1344
+ readonly baseUrl: string | null;
1345
+ /** The context responses the interpretation used. */
1346
+ readonly contexts: readonly SerializedContextSnapshotEntry[];
1347
+ /** The digest of the expanded activity. */
1348
+ readonly expandedDigest: string;
1349
+ }
1350
+ //#endregion
1093
1351
  //#region src/federation/handler.d.ts
1094
1352
  /**
1095
1353
  * Options for the {@link respondWithObject} and
@@ -1504,6 +1762,21 @@ interface InboxMessage {
1504
1762
  * @internal
1505
1763
  */
1506
1764
  readonly ldSignatureVerified?: boolean;
1765
+ /**
1766
+ * The interpretation of the activity that Fedify authenticated, i.e., which
1767
+ * document it was parsed from and every JSON-LD context response the parse
1768
+ * used, so that the worker and its retries dispatch the very same
1769
+ * interpretation without loading contexts again. Absent in messages queued
1770
+ * by older Fedify versions.
1771
+ * @internal
1772
+ */
1773
+ readonly verifiedView?: SerializedVerifiedActivityView;
1774
+ /**
1775
+ * Who Fedify authenticated for the activity. Absent in messages queued by
1776
+ * older Fedify versions.
1777
+ * @internal
1778
+ */
1779
+ readonly authentication?: SerializedInboxAuthentication;
1507
1780
  readonly started: string;
1508
1781
  readonly attempt: number;
1509
1782
  readonly identifier: string | null;
@@ -2884,11 +3157,27 @@ interface InboxListenerSetters<TContextData> {
2884
3157
  /**
2885
3158
  * Registers a listener for a specific incoming activity type.
2886
3159
  *
3160
+ * @example
3161
+ * ``` typescript
3162
+ * federation
3163
+ * .setInboxListeners("/users/{identifier}/inbox", "/inbox")
3164
+ * // Mastodon sends FeatureRequest activities without an actor:
3165
+ * .on(FeatureRequest, async (ctx, request) => {
3166
+ * const verification = await featureInteraction.verifyRequest(ctx, {
3167
+ * request,
3168
+ * authentication: ctx.authentication,
3169
+ * });
3170
+ * // ...
3171
+ * }, { allowActorless: true });
3172
+ * ```
3173
+ *
2887
3174
  * @param type A subclass of {@link Activity} to listen to.
2888
3175
  * @param listener A callback to handle an incoming activity.
3176
+ * @param options Options for the listener. See
3177
+ * {@link InboxListenerOptions}. (Since 2.5.0.)
2889
3178
  * @returns The setters object so that settings can be chained.
2890
3179
  */
2891
- on<TActivity extends Activity>(type: new (...args: any[]) => TActivity, listener: InboxListener<TContextData, TActivity>): InboxListenerSetters<TContextData>;
3180
+ on<TActivity extends Activity>(type: new (...args: any[]) => TActivity, listener: InboxListener<TContextData, TActivity>, options?: InboxListenerOptions): InboxListenerSetters<TContextData>;
2892
3181
  /**
2893
3182
  * Registers an error handler for inbox listeners. Any exceptions thrown
2894
3183
  * from the listeners are caught and passed to this handler.
@@ -4296,6 +4585,11 @@ interface RequestContext<TContextData> extends Context<TContextData> {
4296
4585
  * Gets the public key of the sender, if any exists and it is verified.
4297
4586
  * Otherwise, `null` is returned.
4298
4587
  *
4588
+ * Successful results are reused for this request regardless of the options.
4589
+ * Pending checks and `null` results are reused for the same effective
4590
+ * document and context loaders, including failed lookups. Rejected checks
4591
+ * can be retried. Shared work uses the first caller's tracer provider.
4592
+ *
4299
4593
  * This can be used for implementing [authorized fetch] (also known as
4300
4594
  * secure mode) in ActivityPub.
4301
4595
  *
@@ -4309,6 +4603,11 @@ interface RequestContext<TContextData> extends Context<TContextData> {
4309
4603
  * Gets the public key of the sender, if any exists and it is verified.
4310
4604
  * Otherwise, `null` is returned.
4311
4605
  *
4606
+ * Successful results are reused for this request regardless of the options.
4607
+ * Pending checks and `null` results are reused for the same effective
4608
+ * document and context loaders, including failed lookups. Rejected checks
4609
+ * can be retried. Shared work uses the first caller's tracer provider.
4610
+ *
4312
4611
  * This can be used for implementing [authorized fetch] (also known as
4313
4612
  * secure mode) in ActivityPub.
4314
4613
  *
@@ -4328,6 +4627,14 @@ interface RequestContext<TContextData> extends Context<TContextData> {
4328
4627
  * Gets the owner of the signed key, if any exists and it is verified.
4329
4628
  * Otherwise, `null` is returned.
4330
4629
  *
4630
+ * Verified keys and owners are reused for this request regardless of the
4631
+ * options. Pending checks are shared for the same effective document and
4632
+ * context loaders. A `null` result from an owner lookup after key
4633
+ * verification is retained for those loaders. A `null` result caused by
4634
+ * an unverified key is not retained, allowing an owner lookup after another
4635
+ * loader verifies the key. Rejected checks can be retried. Shared work
4636
+ * uses the first caller's tracer provider.
4637
+ *
4331
4638
  * This can be used for implementing [authorized fetch] (also known as
4332
4639
  * secure mode) in ActivityPub.
4333
4640
  *
@@ -4342,6 +4649,14 @@ interface RequestContext<TContextData> extends Context<TContextData> {
4342
4649
  * Gets the owner of the signed key, if any exists and it is verified.
4343
4650
  * Otherwise, `null` is returned.
4344
4651
  *
4652
+ * Verified keys and owners are reused for this request regardless of the
4653
+ * options. Pending checks are shared for the same effective document and
4654
+ * context loaders. A `null` result from an owner lookup after key
4655
+ * verification is retained for those loaders. A `null` result caused by
4656
+ * an unverified key is not retained, allowing an owner lookup after another
4657
+ * loader verifies the key. Rejected checks can be retried. Shared work
4658
+ * uses the first caller's tracer provider.
4659
+ *
4345
4660
  * This can be used for implementing [authorized fetch] (also known as
4346
4661
  * secure mode) in ActivityPub.
4347
4662
  *
@@ -4417,6 +4732,17 @@ interface InboxContext<TContextData> extends Context<TContextData> {
4417
4732
  */
4418
4733
  readonly recipient: string | null;
4419
4734
  /**
4735
+ * Who Fedify authenticated for the incoming activity, independently of its
4736
+ * declared `actor`, and the interpretation of the activity that was
4737
+ * authenticated. It is preserved through the inbox queue and its retries.
4738
+ *
4739
+ * Use it with {@link isActorAuthenticated} to authorize requests that do
4740
+ * not declare an `actor`, which reach a listener only if it is registered
4741
+ * with the `allowActorless` option.
4742
+ * @since 2.5.0
4743
+ */
4744
+ readonly authentication: InboxAuthenticationEvidence;
4745
+ /**
4420
4746
  * Creates a new context with the same properties as this one,
4421
4747
  * but with the given data.
4422
4748
  * @param data The new data to associate with the context.
@@ -4836,4 +5162,4 @@ interface ActorKeyPair extends CryptoKeyPair {
4836
5162
  readonly multikey: Multikey;
4837
5163
  }
4838
5164
  //#endregion
4839
- export { respondWithObject as $, digest as $t, InboxChallengePolicy as A, CustomCollectionDispatcher as At, createFederation as B, OutboxListener as Bt, FederationBenchmarkOptions as C, ActorKeyPairsDispatcher as Ct, FederationStartQueueOptions as D, CollectionDispatcher as Dt, FederationOptions as E, CollectionCursor as Et, PortableInboxForwardingOptions as F, MediaUploaderCallback as Ft, TaskEnqueueOptions as G, SharedInboxKeyDispatcher as Gt, createFederationBuilder as H, OutboxPermanentFailureHandler as Ht, Rfc6570Expression as I, NodeInfoDispatcher as It, CreateExponentialBackoffPolicyOptions as J, WebFingerLinksDispatcher as Jt, TaskHandler as K, UnverifiedActivityHandler as Kt, FederationKvPrefixes as L, ObjectAuthorizePredicate as Lt, MediaUploaderSetters as M, HashlinkMediaRequest as Mt, ObjectCallbackSetters as N, InboxErrorHandler as Nt, IdempotencyKeyCallback as O, CustomCollectionCounter as Ot, OutboxListenerSetters as P, InboxListener as Pt, RespondWithObjectOptions as Q, buildCollectionSynchronizationHeader as Qt, FederationOrigin as R, ObjectDispatcher as Rt, Federation as S, ActorHandleMapper as St, FederationFetchOptions as T, CollectionCounter as Tt, TaskDefinition as U, PortableActorIdMapper as Ut, Message as V, OutboxListenerErrorHandler as Vt, TaskDefinitionOptions as W, PortableCollectionOwnerMapper as Wt, RetryPolicy as X, SenderKeyPair as Xt, RetryContext as Y, SendActivityError as Yt, createExponentialBackoffPolicy as Z, PageItems as Zt, ActorCallbackSetters as _, NormalizedCircuitBreakerOptions as _t, GetObjectOptions as a, InboxRequestReport as an, MessageQueueEnqueueOptions as at, CustomCollectionCallbackSetters as b, ActorAliasMapper as bt, IsSignedByAudienceOptions as c, CircuitBreaker as ct, ParseUriResult as d, CircuitBreakerCreateOptions as dt, ActivityTransformer as en, respondWithObjectIfAcceptable as et, PortableRequest as f, CircuitBreakerFailurePolicy as ft, SendActivityOptionsForCollection as g, CircuitBreakerStateChange as gt, SendActivityOptions as h, CircuitBreakerState as ht, GetActorOptions as i, InboxRequestOutcome as in, MessageQueueDepth as it, InboxListenerSetters as j, HashlinkMediaDispatcher as jt, IdempotencyStrategy as k, CustomCollectionCursor as kt, OutboxContext as l, CircuitBreakerActivityDrop as lt, RouteActivityOptions as m, CircuitBreakerOptions as mt, Context as n, InboxJsonValue as nn, InProcessMessageQueueOptions as nt, GetSignedKeyOptions as o, MessageQueueListenOptions as ot, RequestContext as p, CircuitBreakerKvState as pt, TaskRegistry as q, UnverifiedActivityReason as qt, ForwardActivityOptions as r, InboxRequestFinishedHandler as rn, MessageQueue as rt, InboxContext as s, ParallelMessageQueue as st, ActorKeyPair as t, InboxAuthentication as tn, InProcessMessageQueue as tt, ParseUriOptions as u, CircuitBreakerBeforeSendDecision as ut, CollectionCallbackSetters as v, normalizeCircuitBreakerOptions as vt, FederationBuilder as w, AuthorizePredicate as wt, Federatable as x, ActorDispatcher as xt, ConstructorWithTypeId as y, parseCircuitBreakerKvState as yt, FederationQueueOptions as z, OutboxErrorHandler as zt };
5165
+ export { respondWithObject as $, SendActivityError as $t, InboxChallengePolicy as A, CollectionCursor as At, createFederation as B, NodeInfoDispatcher as Bt, FederationBenchmarkOptions as C, parseCircuitBreakerKvState as Ct, FederationStartQueueOptions as D, ActorKeyPairsDispatcher as Dt, FederationOptions as E, ActorHandleMapper as Et, PortableInboxForwardingOptions as F, HashlinkMediaDispatcher as Ft, TaskEnqueueOptions as G, OutboxListenerErrorHandler as Gt, createFederationBuilder as H, ObjectDispatcher as Ht, Rfc6570Expression as I, HashlinkMediaRequest as It, CreateExponentialBackoffPolicyOptions as J, PortableCollectionOwnerMapper as Jt, TaskHandler as K, OutboxPermanentFailureHandler as Kt, FederationKvPrefixes as L, InboxErrorHandler as Lt, MediaUploaderSetters as M, CustomCollectionCounter as Mt, ObjectCallbackSetters as N, CustomCollectionCursor as Nt, IdempotencyKeyCallback as O, AuthorizePredicate as Ot, OutboxListenerSetters as P, CustomCollectionDispatcher as Pt, RespondWithObjectOptions as Q, WebFingerLinksDispatcher as Qt, FederationOrigin as R, InboxListener as Rt, Federation as S, normalizeCircuitBreakerOptions as St, FederationFetchOptions as T, ActorDispatcher as Tt, TaskDefinition as U, OutboxErrorHandler as Ut, Message as V, ObjectAuthorizePredicate as Vt, TaskDefinitionOptions as W, OutboxListener as Wt, RetryPolicy as X, UnverifiedActivityHandler as Xt, RetryContext as Y, SharedInboxKeyDispatcher as Yt, createExponentialBackoffPolicy as Z, UnverifiedActivityReason as Zt, ActorCallbackSetters as _, CircuitBreakerKvState as _t, GetObjectOptions as a, InboxAuthentication as an, InProcessMessageQueue as at, CustomCollectionCallbackSetters as b, CircuitBreakerStateChange as bt, IsSignedByAudienceOptions as c, InboxRequestOutcome as cn, MessageQueueDepth as ct, ParseUriResult as d, ParallelMessageQueue as dt, SenderKeyPair as en, respondWithObjectIfAcceptable as et, PortableRequest as f, CircuitBreaker as ft, SendActivityOptionsForCollection as g, CircuitBreakerFailurePolicy as gt, SendActivityOptions as h, CircuitBreakerCreateOptions as ht, GetActorOptions as i, ActivityTransformer as in, isActorAuthenticated as it, InboxListenerSetters as j, CollectionDispatcher as jt, IdempotencyStrategy as k, CollectionCounter as kt, OutboxContext as l, InboxRequestReport as ln, MessageQueueEnqueueOptions as lt, RouteActivityOptions as m, CircuitBreakerBeforeSendDecision as mt, Context as n, buildCollectionSynchronizationHeader as nn, InboxPrincipal as nt, GetSignedKeyOptions as o, InboxJsonValue as on, InProcessMessageQueueOptions as ot, RequestContext as p, CircuitBreakerActivityDrop as pt, TaskRegistry as q, PortableActorIdMapper as qt, ForwardActivityOptions as r, digest as rn, IsActorAuthenticatedOptions as rt, InboxContext as s, InboxRequestFinishedHandler as sn, MessageQueue as st, ActorKeyPair as t, PageItems as tn, InboxAuthenticationEvidence as tt, ParseUriOptions as u, InboxListenerOptions as un, MessageQueueListenOptions as ut, CollectionCallbackSetters as v, CircuitBreakerOptions as vt, FederationBuilder as w, ActorAliasMapper as wt, Federatable as x, NormalizedCircuitBreakerOptions as xt, ConstructorWithTypeId as y, CircuitBreakerState as yt, FederationQueueOptions as z, MediaUploaderCallback as zt };
@@ -3,6 +3,6 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  //#region deno.json
5
5
  var name = "@fedify/fedify";
6
- var version = "2.5.0-dev.2446+2014cb03";
6
+ var version = "2.5.0-dev.2492+4648c04e";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -0,0 +1,136 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { D as validateCryptoKey } from "./ld-CdvcOEXQ.mjs";
5
+ import { r as doubleKnockScoped } from "./http-BoJCMnjY.mjs";
6
+ import { getLogger } from "@logtape/logtape";
7
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, resolveDocumentLoaderTimeout, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
8
+ import { curry } from "es-toolkit";
9
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
10
+ //#region src/utils/docloader.ts
11
+ const logger = getLogger([
12
+ "fedify",
13
+ "utils",
14
+ "docloader"
15
+ ]);
16
+ const DEFAULT_MAX_REDIRECTION = 20;
17
+ /**
18
+ * Gets an authenticated {@link DocumentLoader} for the given identity.
19
+ * Note that an authenticated document loader intentionally does not cache
20
+ * the fetched documents.
21
+ * At most 20 HTTP redirects and alternate document links are followed in total
22
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
23
+ * Each call, including its double-knocking retries, times out after
24
+ * 10 seconds by default; see {@link DocumentLoaderFactoryOptions.timeout}.
25
+ * @param identity The identity to get the document loader for.
26
+ * The actor's key pair.
27
+ * @param options The options for the document loader.
28
+ * @returns The authenticated document loader.
29
+ * @throws {TypeError} If the key is invalid or unsupported.
30
+ * @throws {RangeError} If the `timeout` option is invalid.
31
+ * @since 0.4.0
32
+ */
33
+ function getAuthenticatedDocumentLoader(identity, options = {}) {
34
+ return createAuthenticatedDocumentLoader(identity, options);
35
+ }
36
+ /**
37
+ * The placeholder that the alternate document callback of
38
+ * `getRemoteDocument()` returns, so that the alternate document is loaded
39
+ * after the current hop is closed.
40
+ */
41
+ const ALTERNATE_PLACEHOLDER = Object.freeze({
42
+ contextUrl: null,
43
+ document: null,
44
+ documentUrl: "about:blank"
45
+ });
46
+ /**
47
+ * The implementation of {@link getAuthenticatedDocumentLoader}. This is not
48
+ * exported from any entry point.
49
+ * @param identity The identity to get the document loader for.
50
+ * @param options The options for the document loader.
51
+ * @param deps The dependencies, for tests.
52
+ * @returns The authenticated document loader.
53
+ * @throws {TypeError} If the key is invalid or unsupported.
54
+ */
55
+ function createAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}, deps = {}) {
56
+ validateCryptoKey(identity.privateKey);
57
+ const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
58
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
59
+ options?.signal?.throwIfAborted();
60
+ let currentUrl = new URL(url).href;
61
+ validateUrl(currentUrl);
62
+ visited.add(currentUrl);
63
+ const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
64
+ function follow(nextUrl) {
65
+ options?.signal?.throwIfAborted();
66
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
67
+ if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
68
+ redirected++;
69
+ }
70
+ const result = await doubleKnockScoped(originalRequest, identity, {
71
+ maxRedirection,
72
+ specDeterminer,
73
+ log: curry(logRequest)(logger),
74
+ tracerProvider,
75
+ signal: options?.signal,
76
+ validateRedirect: (nextUrl) => {
77
+ follow(nextUrl);
78
+ validateUrl(nextUrl);
79
+ visited.add(nextUrl);
80
+ currentUrl = nextUrl;
81
+ }
82
+ }, (hopUrl) => openHop(hopUrl, options), async (response) => {
83
+ options?.signal?.throwIfAborted();
84
+ let alternateUrl;
85
+ const document = await getRemoteDocument(currentUrl, response, (url) => {
86
+ alternateUrl = url;
87
+ return Promise.resolve(ALTERNATE_PLACEHOLDER);
88
+ }, options);
89
+ if (alternateUrl == null) return { document };
90
+ if (!response.bodyUsed && response.body?.locked !== true) response.body?.cancel().catch((error) => {
91
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
92
+ url: currentUrl,
93
+ error
94
+ });
95
+ });
96
+ return { alternateUrl };
97
+ });
98
+ options?.signal?.throwIfAborted();
99
+ if ("document" in result) return result.document;
100
+ follow(result.alternateUrl);
101
+ return load(result.alternateUrl, options, redirected, visited);
102
+ }
103
+ function openUnvalidatedHop(url, signal) {
104
+ if (deps.openHop != null) return deps.openHop(url);
105
+ return allowPrivateAddress ? openDirectFetch(url) : openPublicFetch(url, { signal });
106
+ }
107
+ async function openHop(url, options) {
108
+ try {
109
+ const session = await openUnvalidatedHop(url, options?.signal);
110
+ if (options?.signal?.aborted) {
111
+ await session.close();
112
+ options.signal.throwIfAborted();
113
+ }
114
+ return session;
115
+ } catch (error) {
116
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
117
+ url,
118
+ error
119
+ });
120
+ else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
121
+ url,
122
+ error
123
+ });
124
+ throw error;
125
+ }
126
+ }
127
+ function validateUrl(url) {
128
+ const parsedUrl = new URL(url);
129
+ const protocol = parsedUrl.protocol;
130
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
131
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
132
+ }
133
+ return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
134
+ }
135
+ //#endregion
136
+ export { getAuthenticatedDocumentLoader as n, createAuthenticatedDocumentLoader as t };
@@ -189,33 +189,6 @@ var FedifySpanExporter = class {
189
189
  record.spanId
190
190
  ];
191
191
  await this.#kv.set(key, record, options);
192
- await this.#updateTraceSummary(record, options);
193
- }
194
- async #setWithCasRetry(key, transform, options) {
195
- if (this.#kv.cas != null) for (let attempt = 0; attempt < 3; attempt++) {
196
- const existing = await this.#kv.get(key);
197
- const newValue = transform(existing);
198
- if (await this.#kv.cas(key, existing, newValue, options)) return;
199
- }
200
- const newValue = transform(await this.#kv.get(key));
201
- await this.#kv.set(key, newValue, options);
202
- }
203
- async #updateTraceSummary(record, options) {
204
- const summaryKey = [
205
- ...this.#keyPrefix,
206
- "_summaries",
207
- record.traceId
208
- ];
209
- await this.#setWithCasRetry(summaryKey, (existing) => {
210
- const activityCount = existing != null ? existing.activityCount + 1 : 1;
211
- const activityTypes = existing != null ? existing.activityTypes.includes(record.activityType) ? existing.activityTypes : [...existing.activityTypes, record.activityType] : [record.activityType];
212
- return {
213
- traceId: existing?.traceId ?? record.traceId,
214
- timestamp: existing?.timestamp ?? record.timestamp,
215
- activityCount,
216
- activityTypes
217
- };
218
- }, options);
219
192
  }
220
193
  /**
221
194
  * Gets all activity records for a specific trace ID.
@@ -232,13 +205,46 @@ var FedifySpanExporter = class {
232
205
  /**
233
206
  * Gets recent traces with summary information.
234
207
  *
208
+ * Each summary is computed directly from the activity records currently
209
+ * stored for each trace, rather than from a separately maintained
210
+ * counter. This guarantees that a trace is discoverable, and its
211
+ * `activityCount` accurate, as soon as its records are stored, even if
212
+ * an earlier attempt at summarizing it (in a previous version of this
213
+ * exporter) never completed, and that the count always reflects
214
+ * records that have not expired. It costs a full scan of every stored
215
+ * record on every call, including full inbound activity JSON. The
216
+ * `limit` option only truncates the returned summaries and does not
217
+ * reduce the scan. Callers that poll this method frequently should
218
+ * account for the total retained data when choosing a TTL and polling
219
+ * interval.
220
+ *
235
221
  * @param options Options for the query.
236
222
  * @returns An array of trace summaries.
237
223
  */
238
224
  async getRecentTraces(options) {
239
- const summaryPrefix = [...this.#keyPrefix, "_summaries"];
240
- const summaries = [];
241
- for await (const entry of this.#kv.list(summaryPrefix)) summaries.push(entry.value);
225
+ const aggregates = /* @__PURE__ */ new Map();
226
+ const traceIdIndex = this.#keyPrefix.length;
227
+ for await (const entry of this.#kv.list(this.#keyPrefix)) {
228
+ const traceId = entry.key[traceIdIndex];
229
+ if (traceId == null || traceId === "_summaries") continue;
230
+ const record = entry.value;
231
+ if (record == null || typeof record.timestamp !== "string") continue;
232
+ const aggregate = aggregates.get(traceId);
233
+ if (aggregate == null) aggregates.set(traceId, {
234
+ timestamp: record.timestamp,
235
+ activityCount: 1,
236
+ activityTypes: [record.activityType]
237
+ });
238
+ else {
239
+ aggregate.activityCount++;
240
+ if (record.timestamp < aggregate.timestamp) aggregate.timestamp = record.timestamp;
241
+ if (!aggregate.activityTypes.includes(record.activityType)) aggregate.activityTypes.push(record.activityType);
242
+ }
243
+ }
244
+ const summaries = [...aggregates].map(([traceId, aggregate]) => ({
245
+ traceId,
246
+ ...aggregate
247
+ }));
242
248
  summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
243
249
  if (options?.limit != null) return summaries.slice(0, options.limit);
244
250
  return summaries;