@fedify/fedify 2.4.0-dev.1668 → 2.4.0-dev.1740

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 (151) hide show
  1. package/README.md +1 -1
  2. package/dist/{assert-CJ1KXcvv.mjs → assert-CebeUf0C.mjs} +1 -1
  3. package/dist/assert_exists-DtzkXC47.mjs +28 -0
  4. package/dist/assert_false-3pLLLsO4.mjs +23 -0
  5. package/dist/assert_greater_or_equal-BApOTh1h.mjs +30 -0
  6. package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-g3WcyYNR.mjs} +1 -1
  7. package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-BN7yDMWe.mjs} +1 -1
  8. package/dist/assert_rejects-wFbSPn1O.mjs +39 -0
  9. package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-Bpjl4Psw.mjs} +1 -1
  10. package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BJ5wuvR2.mjs} +1 -1
  11. package/dist/{builder-iXZ5jNVg.mjs → builder-Bw6LVKGw.mjs} +5 -3
  12. package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-BT7CiPbz.mjs} +191 -9
  13. package/dist/circuit-breaker-test-utils-BYIHoliq.mjs +14 -0
  14. package/dist/{client-CjF5IYQS.mjs → client-CtxvmazV.mjs} +63 -5
  15. package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
  16. package/dist/compat/mod.cjs +1 -1
  17. package/dist/compat/mod.d.cts +1 -1
  18. package/dist/compat/mod.d.ts +1 -1
  19. package/dist/compat/mod.js +1 -1
  20. package/dist/compat/outgoing-jsonld.test.mjs +3 -3
  21. package/dist/compat/public-audience.test.mjs +3 -3
  22. package/dist/compat/transformers.test.mjs +5 -5
  23. package/dist/{context-C05Rgxmu.d.cts → context-B-dsBjZ9.d.cts} +281 -7
  24. package/dist/{context-Cjnr0ig1.d.ts → context-DrLXudbA.d.ts} +281 -7
  25. package/dist/{deno-DtHcfB0f.mjs → deno-tRdp8MCl.mjs} +1 -1
  26. package/dist/{docloader-0vAfF6XW.mjs → docloader-Dg_7gMC7.mjs} +2 -2
  27. package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
  28. package/dist/federation/builder.test.mjs +18 -5
  29. package/dist/federation/circuit-breaker.test.mjs +626 -7
  30. package/dist/federation/collection.test.mjs +3 -3
  31. package/dist/federation/handler.test.mjs +14 -13
  32. package/dist/federation/idempotency.test.mjs +6 -6
  33. package/dist/federation/inbox.test.mjs +3 -3
  34. package/dist/federation/keycache.test.mjs +5 -5
  35. package/dist/federation/kv.test.mjs +3 -3
  36. package/dist/federation/metrics.test.mjs +4 -4
  37. package/dist/federation/middleware.test.mjs +37 -28
  38. package/dist/federation/mod.cjs +18 -540
  39. package/dist/federation/mod.d.cts +3 -4
  40. package/dist/federation/mod.d.ts +3 -4
  41. package/dist/federation/mod.js +1 -1
  42. package/dist/federation/mq.test.mjs +32 -5
  43. package/dist/federation/negotiation.test.mjs +4 -4
  44. package/dist/federation/retry.test.mjs +3 -3
  45. package/dist/federation/router.test.mjs +7 -7
  46. package/dist/federation/send.test.mjs +28 -26
  47. package/dist/federation/tasks/codec.test.mjs +4 -4
  48. package/dist/federation/tasks/enqueue.test.mjs +4 -4
  49. package/dist/federation/tasks/tasks.test.mjs +6 -6
  50. package/dist/federation/temporal.test.mjs +4 -4
  51. package/dist/federation/webfinger.test.mjs +74 -12
  52. package/dist/{middleware-DzIUFRO2.cjs → federation-BJBpt_Am.cjs} +855 -114
  53. package/dist/{http-BbnOMQoy.js → http-BIxGa6AG.js} +3 -3
  54. package/dist/{http-DUmWq5Pw.mjs → http-Day1kcpT.mjs} +5 -5
  55. package/dist/{temporal-polyfill-BCdTHexd.cjs → http-DggG1scN.cjs} +2801 -104
  56. package/dist/{key-CM7wDnZG.mjs → key-igh0AMsL.mjs} +2 -2
  57. package/dist/{kv-cache-ZQ22hm44.js → kv-cache-BIAB0HKx.js} +1 -1
  58. package/dist/{kv-cache-DEXKemJ9.mjs → kv-cache-Bz6aQOOX.mjs} +1 -1
  59. package/dist/{kv-cache-zmgG1HUp.cjs → kv-cache-C4U_uBqD.cjs} +2 -4
  60. package/dist/{ld-BttH_-8K.mjs → ld-DnT9sCWa.mjs} +5 -5
  61. package/dist/metrics-DVKYY6K1.d.cts +36 -0
  62. package/dist/{metrics-DT-CnD1g.mjs → metrics-DbdNnWIt.mjs} +2 -2
  63. package/dist/metrics-DoRtI0qU.d.ts +36 -0
  64. package/dist/{middleware-ChlYS0Af.mjs → middleware-B6GweVcg.mjs} +1 -1
  65. package/dist/{middleware-D3IAVMcT.js → middleware-CnsPXG0O.js} +241 -40
  66. package/dist/{middleware-DrroX3aD.mjs → middleware-DPIS4fsB.mjs} +60 -43
  67. package/dist/{mod-BrpVsSfY.d.cts → mod-CLHkCwLm.d.cts} +3 -3
  68. package/dist/{mod-B0hW12_O.d.cts → mod-CnGLNoVw.d.cts} +86 -4
  69. package/dist/{mod-DdONo-ep.d.cts → mod-D-abvu2C.d.cts} +1 -1
  70. package/dist/{mod-BsCE5Ihq.d.ts → mod-D6O8C7eN.d.ts} +1 -1
  71. package/dist/{mod-CNef4in0.d.ts → mod-DypxPS9H.d.ts} +3 -3
  72. package/dist/{mod-COIAjwRS.d.ts → mod-DzRXACM5.d.ts} +86 -4
  73. package/dist/mod.cjs +25 -25
  74. package/dist/mod.d.cts +10 -11
  75. package/dist/mod.d.ts +10 -11
  76. package/dist/mod.js +8 -8
  77. package/dist/nodeinfo/client.test.mjs +64 -4
  78. package/dist/nodeinfo/handler.test.mjs +5 -5
  79. package/dist/nodeinfo/mod.cjs +1 -1
  80. package/dist/nodeinfo/mod.d.cts +1 -1
  81. package/dist/nodeinfo/mod.d.ts +1 -1
  82. package/dist/nodeinfo/mod.js +1 -1
  83. package/dist/nodeinfo/types.test.mjs +4 -4
  84. package/dist/otel/exporter.test.mjs +3 -3
  85. package/dist/otel/mod.cjs +0 -1
  86. package/dist/otel/mod.d.cts +1 -1
  87. package/dist/otel/mod.d.ts +1 -1
  88. package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-BQSHRLhg.mjs} +2 -2
  89. package/dist/{owner-DZALmhdl.mjs → owner-B3ef8GfJ.mjs} +2 -2
  90. package/dist/{proof-CqA-2nfW.js → proof-BBLQXRWw.js} +608 -36
  91. package/dist/{proof-a60oVJ_4.cjs → proof-Cqk2TcLX.cjs} +616 -39
  92. package/dist/proof-DVoDZ8d7.mjs +890 -0
  93. package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--9hx7IGx.mjs} +1 -1
  94. package/dist/runtime/mod.cjs +1 -1
  95. package/dist/runtime/mod.js +1 -1
  96. package/dist/{send-DcwZnQWG.mjs → send-PbBSZ7Qk.mjs} +3 -3
  97. package/dist/sig/accept.test.mjs +1 -1
  98. package/dist/sig/http.test.mjs +37 -11
  99. package/dist/sig/key.test.mjs +6 -6
  100. package/dist/sig/ld.test.mjs +9 -7
  101. package/dist/sig/mod.cjs +3 -2
  102. package/dist/sig/mod.d.cts +4 -4
  103. package/dist/sig/mod.d.ts +4 -4
  104. package/dist/sig/mod.js +3 -3
  105. package/dist/sig/owner.test.mjs +6 -6
  106. package/dist/sig/proof.test.mjs +965 -15
  107. package/dist/std__assert-CxFT0MNt.mjs +8 -0
  108. package/dist/{temporal-Bpxpsl2k.mjs → temporal-DLQIjuTn.mjs} +3 -3
  109. package/dist/testing/mod.d.mts +10 -0
  110. package/dist/testing/mod.mjs +2 -2
  111. package/dist/{testing-CO7ftO8A.mjs → testing-BX0Nicyq.mjs} +2 -2
  112. package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
  113. package/dist/{types-DC1pNgJA.js → types-Dtyf8lVj.js} +63 -5
  114. package/dist/{types-CPym83zc.cjs → types-RXm5YxpC.cjs} +62 -5
  115. package/dist/utils/docloader.test.mjs +7 -7
  116. package/dist/utils/kv-cache.test.mjs +1 -1
  117. package/dist/utils/mod.cjs +1 -1
  118. package/dist/utils/mod.d.cts +1 -1
  119. package/dist/utils/mod.d.ts +1 -1
  120. package/dist/utils/mod.js +1 -1
  121. package/package.json +7 -7
  122. package/skills/fedify/SKILL.md +462 -0
  123. package/dist/assert_rejects-EgstcQy5.mjs +0 -82
  124. package/dist/http-BpT5cE6r.cjs +0 -2694
  125. package/dist/metrics-DN13mCAw.d.cts +0 -294
  126. package/dist/metrics-v_e0n5GQ.d.ts +0 -294
  127. package/dist/proof-DiQ0uBal.mjs +0 -316
  128. package/dist/std__assert-B2NnY9bq.mjs +0 -79
  129. /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
  130. /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
  131. /package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-CBlRfA-_.mjs} +0 -0
  132. /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
  133. /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
  134. /package/dist/{codec-BBga-FWI.mjs → codec-peqOdBaE.mjs} +0 -0
  135. /package/dist/{context-BJXxPElm.mjs → context-Dt36KFEJ.mjs} +0 -0
  136. /package/dist/{http-VyDTd4G3.d.cts → http-6TI0tI03.d.cts} +0 -0
  137. /package/dist/{http-lf8Hsd91.d.ts → http-CdrSn2ip.d.ts} +0 -0
  138. /package/dist/{keycache-_DHqz5z_.mjs → keycache-0Jk78XmB.mjs} +0 -0
  139. /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
  140. /package/dist/{kv-D6hNiMTK.d.ts → kv-BhNbN8Ed.d.cts} +0 -0
  141. /package/dist/{kv-gJ8LYbxX.d.cts → kv-BhNbN8Ed.d.ts} +0 -0
  142. /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
  143. /package/dist/{mq-Bo7Etgh6.mjs → mq-Dm1Wmtm-.mjs} +0 -0
  144. /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
  145. /package/dist/{owner-CnngXDNJ.d.ts → owner-DGSfntI-.d.ts} +0 -0
  146. /package/dist/{owner-DEvZuyOE.d.cts → owner-r_waTiQj.d.cts} +0 -0
  147. /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
  148. /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
  149. /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
  150. /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
  151. /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
@@ -1,2694 +0,0 @@
1
- const { URLPattern } = require("urlpattern-polyfill");
2
- require("./chunk-Czpeue4M.cjs");
3
- const require_temporal_polyfill = require("./temporal-polyfill-BCdTHexd.cjs");
4
- let _logtape_logtape = require("@logtape/logtape");
5
- let _fedify_vocab = require("@fedify/vocab");
6
- let _opentelemetry_api = require("@opentelemetry/api");
7
- let byte_encodings_hex = require("byte-encodings/hex");
8
- let structured_field_values = require("structured-field-values");
9
- let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
10
- let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
11
- let byte_encodings_base64 = require("byte-encodings/base64");
12
- //#region deno.json
13
- var name = "@fedify/fedify";
14
- var version = "2.4.0-dev.1668+90797b01";
15
- //#endregion
16
- //#region src/sig/accept.ts
17
- /**
18
- * `Accept-Signature` header parsing, serialization, and validation utilities
19
- * for RFC 9421 §5 challenge-response negotiation.
20
- *
21
- * @module
22
- */
23
- /**
24
- * Parses an `Accept-Signature` header value (RFC 9421 §5.1) into an
25
- * array of {@link AcceptSignatureMember} objects.
26
- *
27
- * The `Accept-Signature` field is a Dictionary Structured Field
28
- * (RFC 8941 §3.2). Each dictionary member describes a single
29
- * requested message signature.
30
- *
31
- * On parse failure (malformed or empty header), returns an empty array.
32
- *
33
- * @param header The raw `Accept-Signature` header value string.
34
- * @returns An array of parsed members. Empty if the header is
35
- * malformed or empty.
36
- * @since 2.1.0
37
- */
38
- function parseAcceptSignature(header) {
39
- try {
40
- return parseEachSignature((0, structured_field_values.decodeDict)(header));
41
- } catch {
42
- (0, _logtape_logtape.getLogger)([
43
- "fedify",
44
- "sig",
45
- "http"
46
- ]).warn("Failed to parse Accept-Signature header: {header}", { header });
47
- return [];
48
- }
49
- }
50
- const compactObject = (obj) => Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== void 0));
51
- const parseEachSignature = (dict) => Object.entries(dict).filter(([_, item]) => Array.isArray(item.value)).map(([label, item]) => ({
52
- label,
53
- components: item.value.filter((subitem) => typeof subitem.value === "string").map((subitem) => ({
54
- value: subitem.value,
55
- params: subitem.params ?? {}
56
- })),
57
- parameters: compactParams(item)
58
- }));
59
- const compactParams = (item) => {
60
- const { keyid, alg, created, expires, nonce, tag } = item.params ?? {};
61
- return compactObject({
62
- keyid: stringOrUndefined(keyid),
63
- alg: stringOrUndefined(alg),
64
- created: trueOrUndefined(created),
65
- expires: trueOrUndefined(expires),
66
- nonce: stringOrUndefined(nonce),
67
- tag: stringOrUndefined(tag)
68
- });
69
- };
70
- const stringOrUndefined = (v) => typeof v === "string" ? v : void 0;
71
- const trueOrUndefined = (v) => v === true ? true : void 0;
72
- /**
73
- * Serializes an array of {@link AcceptSignatureMember} objects into an
74
- * `Accept-Signature` header value string (RFC 9421 §5.1).
75
- *
76
- * The output is a Dictionary Structured Field (RFC 8941 §3.2).
77
- *
78
- * @param members The members to serialize.
79
- * @returns The serialized header value string.
80
- * @since 2.1.0
81
- */
82
- function formatAcceptSignature(members) {
83
- const items = members.map((member) => [member.label, new structured_field_values.Item(compToItems(member), compactParameters(member))]);
84
- return (0, structured_field_values.encodeDict)(Object.fromEntries(items));
85
- }
86
- const compToItems = (member) => member.components.map((c) => new structured_field_values.Item(c.value, c.params));
87
- const compactParameters = (member) => {
88
- const { keyid, alg, created, expires, nonce, tag } = member.parameters;
89
- return compactObject({
90
- keyid,
91
- alg,
92
- created,
93
- expires,
94
- nonce,
95
- tag
96
- });
97
- };
98
- /**
99
- * Filters out {@link AcceptSignatureMember} entries whose covered
100
- * components include response-only identifiers (`@status`) that are
101
- * not applicable to request-target messages, as required by
102
- * [RFC 9421 §5](https://www.rfc-editor.org/rfc/rfc9421#section-5).
103
- *
104
- * A warning is logged for each discarded entry.
105
- *
106
- * @param members The parsed `Accept-Signature` entries to validate.
107
- * @returns Only entries that are valid for request-target messages.
108
- * @since 2.1.0
109
- */
110
- function validateAcceptSignature(members) {
111
- const logger = (0, _logtape_logtape.getLogger)([
112
- "fedify",
113
- "sig",
114
- "http"
115
- ]);
116
- return members.filter((member) => {
117
- if (member.components.every((c) => c.value !== "@status")) return true;
118
- logLabel(logger, member.label);
119
- return false;
120
- });
121
- }
122
- const logLabel = (logger, label) => logger.warn("Discarding Accept-Signature member {label}: covered components include response-only identifier @status.", { label });
123
- /**
124
- * Attempts to translate an {@link AcceptSignatureMember} challenge into
125
- * RFC 9421 signing options that the local signer can fulfill.
126
- *
127
- * Returns `null` if the challenge cannot be fulfilled—for example, if
128
- * the requested `alg` or `keyid` is incompatible with the local key.
129
- *
130
- * Safety constraints:
131
- * - `alg`: only honored if it matches `localAlg`.
132
- * - `keyid`: only honored if it matches `localKeyId`.
133
- * - `components`: passed through exactly as requested, per RFC 9421 §5.2.
134
- * - `nonce`, `tag`, and `expires` are passed through directly.
135
- *
136
- * @param entry The challenge entry from the `Accept-Signature` header.
137
- * @param localKeyId The local key identifier (e.g., the actor key URL).
138
- * @param localAlg The algorithm of the local private key
139
- * (e.g., `"rsa-v1_5-sha256"`).
140
- * @returns Signing options if the challenge can be fulfilled, or `null`.
141
- * @since 2.1.0
142
- */
143
- function fulfillAcceptSignature(entry, localKeyId, localAlg) {
144
- if (entry.parameters.alg != null && entry.parameters.alg !== localAlg) return null;
145
- if (entry.parameters.keyid != null && entry.parameters.keyid !== localKeyId) return null;
146
- return {
147
- label: entry.label,
148
- components: entry.components,
149
- nonce: entry.parameters.nonce,
150
- tag: entry.parameters.tag,
151
- expires: entry.parameters.expires
152
- };
153
- }
154
- //#endregion
155
- //#region src/federation/metrics.ts
156
- var FederationMetrics = class {
157
- deliverySent;
158
- deliveryPermanentFailure;
159
- signatureVerificationFailure;
160
- signatureVerificationDuration;
161
- signatureKeyFetchDuration;
162
- deliveryDuration;
163
- inboxProcessingDuration;
164
- httpServerRequestCount;
165
- httpServerRequestDuration;
166
- queueTaskEnqueued;
167
- queueTaskStarted;
168
- queueTaskCompleted;
169
- queueTaskFailed;
170
- queueTaskDuration;
171
- queueTaskInFlight;
172
- queueDepth;
173
- fanoutRecipients;
174
- inboxActivity;
175
- outboxActivity;
176
- circuitBreakerStateChange;
177
- keyLookup;
178
- keyLookupDuration;
179
- documentFetch;
180
- documentFetchDuration;
181
- documentCache;
182
- webFingerHandle;
183
- webFingerHandleDuration;
184
- collectionRequest;
185
- collectionDispatchDuration;
186
- collectionPageItems;
187
- collectionTotalItems;
188
- constructor(meterProvider) {
189
- const meter = meterProvider.getMeter(name, version);
190
- this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
191
- description: "ActivityPub delivery attempts.",
192
- unit: "{attempt}"
193
- });
194
- this.deliveryPermanentFailure = meter.createCounter("activitypub.delivery.permanent_failure", {
195
- description: "ActivityPub deliveries abandoned as permanent failures.",
196
- unit: "{failure}"
197
- });
198
- this.signatureVerificationFailure = meter.createCounter("activitypub.signature.verification_failure", {
199
- description: "ActivityPub signature verification failures.",
200
- unit: "{failure}"
201
- });
202
- this.signatureVerificationDuration = meter.createHistogram("activitypub.signature.verification.duration", {
203
- description: "Duration of ActivityPub signature verification, including local key lookup and remote key fetches.",
204
- unit: "ms",
205
- advice: { explicitBucketBoundaries: [
206
- .1,
207
- .25,
208
- .5,
209
- 1,
210
- 2.5,
211
- 5,
212
- 10,
213
- 25,
214
- 50,
215
- 100,
216
- 250,
217
- 500,
218
- 1e3
219
- ] }
220
- });
221
- this.signatureKeyFetchDuration = meter.createHistogram("activitypub.signature.key_fetch.duration", {
222
- description: "Duration of public key lookup performed during ActivityPub signature verification.",
223
- unit: "ms"
224
- });
225
- this.deliveryDuration = meter.createHistogram("activitypub.delivery.duration", {
226
- description: "Duration of ActivityPub delivery attempts.",
227
- unit: "ms"
228
- });
229
- this.inboxProcessingDuration = meter.createHistogram("activitypub.inbox.processing_duration", {
230
- description: "Duration of ActivityPub inbox listener processing.",
231
- unit: "ms"
232
- });
233
- this.httpServerRequestCount = meter.createCounter("fedify.http.server.request.count", {
234
- description: "HTTP requests handled by Federation.fetch().",
235
- unit: "{request}"
236
- });
237
- this.httpServerRequestDuration = meter.createHistogram("fedify.http.server.request.duration", {
238
- description: "Duration of HTTP requests handled by Federation.fetch().",
239
- unit: "ms",
240
- advice: { explicitBucketBoundaries: [
241
- 5,
242
- 10,
243
- 25,
244
- 50,
245
- 75,
246
- 100,
247
- 250,
248
- 500,
249
- 750,
250
- 1e3,
251
- 2500,
252
- 5e3,
253
- 7500,
254
- 1e4
255
- ] }
256
- });
257
- this.queueTaskEnqueued = meter.createCounter("fedify.queue.task.enqueued", {
258
- description: "Tasks Fedify enqueued for inbox, outbox, or fanout work.",
259
- unit: "{task}"
260
- });
261
- this.queueTaskStarted = meter.createCounter("fedify.queue.task.started", {
262
- description: "Tasks Fedify began processing as a queue worker.",
263
- unit: "{task}"
264
- });
265
- this.queueTaskCompleted = meter.createCounter("fedify.queue.task.completed", {
266
- description: "Queue tasks Fedify finished processing without throwing.",
267
- unit: "{task}"
268
- });
269
- this.queueTaskFailed = meter.createCounter("fedify.queue.task.failed", {
270
- description: "Queue tasks Fedify abandoned because processing threw.",
271
- unit: "{task}"
272
- });
273
- this.queueTaskDuration = meter.createHistogram("fedify.queue.task.duration", {
274
- description: "Duration of queue task processing in Fedify workers.",
275
- unit: "ms",
276
- advice: { explicitBucketBoundaries: [
277
- 5,
278
- 10,
279
- 25,
280
- 50,
281
- 75,
282
- 100,
283
- 250,
284
- 500,
285
- 750,
286
- 1e3,
287
- 2500,
288
- 5e3,
289
- 7500,
290
- 1e4
291
- ] }
292
- });
293
- this.queueTaskInFlight = meter.createUpDownCounter("fedify.queue.task.in_flight", {
294
- description: "Queue tasks currently being processed in this Fedify process.",
295
- unit: "{task}"
296
- });
297
- this.queueDepth = meter.createObservableGauge("fedify.queue.depth", {
298
- description: "Messages waiting in configured Fedify queues, as reported by the queue backend.",
299
- unit: "{message}"
300
- });
301
- this.fanoutRecipients = meter.createHistogram("activitypub.fanout.recipients", {
302
- description: "Number of recipient inboxes produced by an ActivityPub fanout task.",
303
- unit: "{recipient}"
304
- });
305
- this.inboxActivity = meter.createCounter("activitypub.inbox.activity", {
306
- description: "ActivityPub activities observed at the inbox lifecycle level: queued, processed, retried, rejected, or abandoned.",
307
- unit: "{activity}"
308
- });
309
- this.outboxActivity = meter.createCounter("activitypub.outbox.activity", {
310
- description: "ActivityPub activities observed at the outbox lifecycle level: queued, retried, or abandoned. Per-recipient delivery counters live on `activitypub.delivery.*`.",
311
- unit: "{activity}"
312
- });
313
- this.circuitBreakerStateChange = meter.createCounter("activitypub.circuit_breaker.state_change", {
314
- description: "Outbound ActivityPub delivery circuit breaker changes.",
315
- unit: "{change}"
316
- });
317
- this.keyLookup = meter.createCounter("activitypub.key.lookup", {
318
- description: "Public-key lookup attempts performed by Fedify, including cache hits, local resolutions, and remote fetches.",
319
- unit: "{lookup}"
320
- });
321
- this.keyLookupDuration = meter.createHistogram("activitypub.key.lookup.duration", {
322
- description: "Duration of public-key lookups performed by Fedify, including cache hits, local resolutions, and any remote fetch.",
323
- unit: "ms",
324
- advice: { explicitBucketBoundaries: [
325
- 5,
326
- 10,
327
- 25,
328
- 50,
329
- 75,
330
- 100,
331
- 250,
332
- 500,
333
- 750,
334
- 1e3,
335
- 2500,
336
- 5e3,
337
- 7500,
338
- 1e4
339
- ] }
340
- });
341
- this.documentFetch = meter.createCounter("activitypub.document.fetch", {
342
- description: "Remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
343
- unit: "{fetch}"
344
- });
345
- this.documentFetchDuration = meter.createHistogram("activitypub.document.fetch.duration", {
346
- description: "Duration of remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
347
- unit: "ms",
348
- advice: { explicitBucketBoundaries: [
349
- 5,
350
- 10,
351
- 25,
352
- 50,
353
- 75,
354
- 100,
355
- 250,
356
- 500,
357
- 750,
358
- 1e3,
359
- 2500,
360
- 5e3,
361
- 7500,
362
- 1e4
363
- ] }
364
- });
365
- this.documentCache = meter.createCounter("activitypub.document.cache", {
366
- description: "KV-backed document loader cache lookups, with `hit` or `miss` classification.",
367
- unit: "{lookup}"
368
- });
369
- this.webFingerHandle = meter.createCounter("webfinger.handle", {
370
- description: "Incoming WebFinger requests handled by Fedify, classified by terminal outcome.",
371
- unit: "{request}"
372
- });
373
- this.webFingerHandleDuration = meter.createHistogram("webfinger.handle.duration", {
374
- description: "Duration of incoming WebFinger request handling in Fedify.",
375
- unit: "ms",
376
- advice: { explicitBucketBoundaries: [
377
- 5,
378
- 10,
379
- 25,
380
- 50,
381
- 75,
382
- 100,
383
- 250,
384
- 500,
385
- 750,
386
- 1e3,
387
- 2500,
388
- 5e3,
389
- 7500,
390
- 1e4
391
- ] }
392
- });
393
- this.collectionRequest = meter.createCounter("activitypub.collection.request", {
394
- description: "ActivityPub collection and collection-page requests handled by Fedify.",
395
- unit: "{request}"
396
- });
397
- this.collectionDispatchDuration = meter.createHistogram("activitypub.collection.dispatch.duration", {
398
- description: "Duration of ActivityPub collection dispatcher callbacks.",
399
- unit: "ms",
400
- advice: { explicitBucketBoundaries: [
401
- 5,
402
- 10,
403
- 25,
404
- 50,
405
- 75,
406
- 100,
407
- 250,
408
- 500,
409
- 750,
410
- 1e3,
411
- 2500,
412
- 5e3,
413
- 7500,
414
- 1e4
415
- ] }
416
- });
417
- this.collectionPageItems = meter.createHistogram("activitypub.collection.page.items", {
418
- description: "Number of items Fedify materialized for an ActivityPub collection response.",
419
- unit: "{item}"
420
- });
421
- this.collectionTotalItems = meter.createHistogram("activitypub.collection.total_items", {
422
- description: "Total item count reported by ActivityPub collection counters.",
423
- unit: "{item}"
424
- });
425
- }
426
- recordDelivery(inbox, durationMs, success, activityType) {
427
- const deliveryAttributes = {
428
- "activitypub.remote.host": getRemoteHost(inbox),
429
- "activitypub.delivery.success": success
430
- };
431
- if (activityType != null) deliveryAttributes["activitypub.activity.type"] = activityType;
432
- this.deliverySent.add(1, deliveryAttributes);
433
- this.deliveryDuration.record(durationMs, deliveryAttributes);
434
- }
435
- recordPermanentFailure(inbox, statusCode) {
436
- this.deliveryPermanentFailure.add(1, {
437
- "activitypub.remote.host": getRemoteHost(inbox),
438
- "http.response.status_code": statusCode
439
- });
440
- }
441
- recordSignatureVerificationFailure(reason, remoteHost) {
442
- const attributes = { "activitypub.verification.failure_reason": reason };
443
- if (remoteHost != null) attributes["activitypub.remote.host"] = remoteHost;
444
- this.signatureVerificationFailure.add(1, attributes);
445
- }
446
- recordSignatureVerificationDuration(durationMs, kind, result, extra = {}) {
447
- const attributes = {
448
- "activitypub.signature.kind": kind,
449
- "activitypub.signature.result": result
450
- };
451
- if (extra.algorithm != null) attributes["http_signatures.algorithm"] = extra.algorithm;
452
- if (extra.failureReason != null) attributes["http_signatures.failure_reason"] = extra.failureReason;
453
- if (extra.ldType != null) attributes["ld_signatures.type"] = extra.ldType;
454
- if (extra.cryptosuite != null) attributes["object_integrity_proofs.cryptosuite"] = extra.cryptosuite;
455
- this.signatureVerificationDuration.record(durationMs, attributes);
456
- }
457
- recordSignatureKeyFetchDuration(durationMs, kind, result) {
458
- this.signatureKeyFetchDuration.record(durationMs, {
459
- "activitypub.signature.kind": kind,
460
- "activitypub.signature.key_fetch.result": result
461
- });
462
- }
463
- recordInboxProcessingDuration(activityType, durationMs) {
464
- this.inboxProcessingDuration.record(durationMs, { "activitypub.activity.type": activityType });
465
- }
466
- recordHttpServerRequest(method, endpoint, durationMs, options = {}) {
467
- const attributes = {
468
- "http.request.method": normalizeHttpMethod(method),
469
- "fedify.endpoint": endpoint
470
- };
471
- if (options.statusCode != null) attributes["http.response.status_code"] = options.statusCode;
472
- if (options.routeTemplate != null) attributes["fedify.route.template"] = options.routeTemplate;
473
- this.httpServerRequestCount.add(1, attributes);
474
- this.httpServerRequestDuration.record(durationMs, attributes);
475
- }
476
- recordQueueTaskEnqueued(common, attempt, count = 1) {
477
- const attributes = buildQueueTaskAttributes(common);
478
- attributes["fedify.queue.task.attempt"] = attempt;
479
- this.queueTaskEnqueued.add(count, attributes);
480
- }
481
- recordQueueTaskStarted(common) {
482
- this.queueTaskStarted.add(1, buildQueueTaskAttributes(common));
483
- }
484
- incrementQueueTaskInFlight(common) {
485
- this.queueTaskInFlight.add(1, buildQueueTaskInFlightAttributes(common));
486
- }
487
- decrementQueueTaskInFlight(common) {
488
- this.queueTaskInFlight.add(-1, buildQueueTaskInFlightAttributes(common));
489
- }
490
- recordQueueTaskOutcome(common, result, durationMs, failureReason) {
491
- const attributes = buildQueueTaskAttributes(common);
492
- attributes["fedify.queue.task.result"] = result;
493
- if (failureReason != null && result === "failed") attributes["fedify.task.failure_reason"] = failureReason;
494
- if (result === "completed") this.queueTaskCompleted.add(1, attributes);
495
- else if (result === "failed") this.queueTaskFailed.add(1, attributes);
496
- this.queueTaskDuration.record(durationMs, attributes);
497
- }
498
- recordFanoutRecipients(recipientCount, activityType) {
499
- const attributes = {};
500
- if (activityType != null) attributes["activitypub.activity.type"] = activityType;
501
- this.fanoutRecipients.record(recipientCount, attributes);
502
- }
503
- recordInboxActivity(result, activityType) {
504
- this.inboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
505
- }
506
- recordOutboxActivity(result, activityType) {
507
- this.outboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
508
- }
509
- recordCircuitBreakerStateChange(remoteHost, state) {
510
- this.circuitBreakerStateChange.add(1, {
511
- "activitypub.remote.host": remoteHost,
512
- "activitypub.circuit_breaker.state": state
513
- });
514
- }
515
- recordKeyLookup(attrs) {
516
- const attributes = {
517
- "activitypub.lookup.kind": "public_key",
518
- "activitypub.lookup.result": attrs.result,
519
- "activitypub.cache.enabled": attrs.cacheEnabled
520
- };
521
- if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
522
- if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
523
- this.keyLookup.add(1, attributes);
524
- this.keyLookupDuration.record(attrs.durationMs, attributes);
525
- }
526
- recordDocumentFetch(attrs) {
527
- const attributes = {
528
- "activitypub.lookup.kind": attrs.kind,
529
- "activitypub.lookup.result": attrs.result
530
- };
531
- if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
532
- if (attrs.cacheEnabled != null) attributes["activitypub.cache.enabled"] = attrs.cacheEnabled;
533
- if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
534
- this.documentFetch.add(1, attributes);
535
- this.documentFetchDuration.record(attrs.durationMs, attributes);
536
- }
537
- recordDocumentCache(attrs) {
538
- const attributes = {
539
- "activitypub.lookup.kind": attrs.kind,
540
- "activitypub.lookup.result": attrs.result
541
- };
542
- if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
543
- this.documentCache.add(1, attributes);
544
- }
545
- recordWebFingerHandle(attrs) {
546
- const attributes = { "webfinger.handle.result": attrs.result };
547
- if (attrs.scheme != null) attributes["webfinger.resource.scheme"] = attrs.scheme;
548
- if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
549
- this.webFingerHandle.add(1, attributes);
550
- this.webFingerHandleDuration.record(attrs.durationMs, attributes);
551
- }
552
- recordCollectionRequest(attrs) {
553
- this.collectionRequest.add(1, buildCollectionAttributes(attrs));
554
- }
555
- recordCollectionDispatchDuration(durationMs, attrs) {
556
- this.collectionDispatchDuration.record(durationMs, buildCollectionAttributes(attrs));
557
- }
558
- recordCollectionPageItems(itemCount, attrs) {
559
- this.collectionPageItems.record(itemCount, buildCollectionAttributes(attrs));
560
- }
561
- recordCollectionTotalItems(totalItems, attrs) {
562
- this.collectionTotalItems.record(totalItems, buildCollectionAttributes(attrs));
563
- }
564
- };
565
- function buildCollectionAttributes(attrs) {
566
- const attributes = {
567
- "activitypub.collection.kind": attrs.kind,
568
- "activitypub.collection.page": attrs.page,
569
- "activitypub.collection.result": attrs.result,
570
- "fedify.collection.dispatcher": attrs.dispatcher
571
- };
572
- if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
573
- return attributes;
574
- }
575
- function buildActivityLifecycleAttributes(result, activityType) {
576
- const attributes = { "activitypub.processing.result": result };
577
- if (activityType != null) attributes["activitypub.activity.type"] = activityType;
578
- return attributes;
579
- }
580
- function buildQueueTaskAttributes(common) {
581
- const attributes = { "fedify.queue.role": common.role };
582
- const backend = getQueueBackend(common.queue);
583
- if (backend != null) attributes["fedify.queue.backend"] = backend;
584
- const nativeRetrial = common.queue?.nativeRetrial;
585
- if (typeof nativeRetrial === "boolean") attributes["fedify.queue.native_retrial"] = nativeRetrial;
586
- if (common.activityType != null) attributes["activitypub.activity.type"] = common.activityType;
587
- if (common.taskName != null) attributes["fedify.task.name"] = common.taskName;
588
- return attributes;
589
- }
590
- function buildQueueTaskInFlightAttributes(common) {
591
- return buildQueueTaskAttributes({
592
- role: common.role,
593
- queue: common.queue
594
- });
595
- }
596
- /**
597
- * Returns the constructor name of the given message queue, when it is a
598
- * meaningful identifier. Used as a best-effort `fedify.queue.backend`
599
- * attribute on queue task metrics; returns `undefined` for plain object
600
- * literals (whose constructor is `Object`) so the attribute does not appear
601
- * with a non-informative value.
602
- * @since 2.3.0
603
- */
604
- function getQueueBackend(queue) {
605
- const name = queue?.constructor?.name;
606
- if (name == null || name === "" || name === "Object") return void 0;
607
- return name;
608
- }
609
- /**
610
- * Registers a callback for observing queue backend depth.
611
- * @since 2.3.0
612
- */
613
- function registerQueueDepthGauge(meterProvider, entries, options = {}) {
614
- const uniqueQueues = /* @__PURE__ */ new Map();
615
- for (const { role, queue } of entries) {
616
- if (queue?.getDepth == null) continue;
617
- const roles = uniqueQueues.get(queue);
618
- if (roles == null) uniqueQueues.set(queue, [role]);
619
- else if (!roles.includes(role)) roles.push(role);
620
- }
621
- if (uniqueQueues.size < 1) return;
622
- const queueEntries = Array.from(uniqueQueues.entries());
623
- getFederationMetrics(meterProvider).queueDepth.addCallback(async (observableResult) => {
624
- await Promise.all(queueEntries.map(async ([queue, roles]) => {
625
- let depth;
626
- try {
627
- depth = await queue.getDepth();
628
- } catch {
629
- return;
630
- }
631
- if (depth == null) return;
632
- const attributes = buildQueueDepthAttributes(queue, roles, options);
633
- observableResult.observe(depth.queued, {
634
- ...attributes,
635
- "fedify.queue.depth.state": "queued"
636
- });
637
- if (depth.ready != null) observableResult.observe(depth.ready, {
638
- ...attributes,
639
- "fedify.queue.depth.state": "ready"
640
- });
641
- if (depth.delayed != null) observableResult.observe(depth.delayed, {
642
- ...attributes,
643
- "fedify.queue.depth.state": "delayed"
644
- });
645
- }));
646
- });
647
- }
648
- function buildQueueDepthAttributes(queue, roles, options) {
649
- const sortedRoles = roles.toSorted();
650
- const role = sortedRoles.length === 1 ? sortedRoles[0] : "shared";
651
- const attributes = { "fedify.queue.role": role };
652
- if (options.sourceId != null) attributes["fedify.federation.instance_id"] = options.sourceId;
653
- if (role === "shared") attributes["fedify.queue.roles"] = sortedRoles.join(",");
654
- const backend = getQueueBackend(queue);
655
- if (backend != null) attributes["fedify.queue.backend"] = backend;
656
- const nativeRetrial = queue.nativeRetrial;
657
- if (typeof nativeRetrial === "boolean") attributes["fedify.queue.native_retrial"] = nativeRetrial;
658
- return attributes;
659
- }
660
- /**
661
- * Records `fedify.queue.task.enqueued` for an outgoing outbox enqueue and,
662
- * for the initial attempt, also records
663
- * `activitypub.outbox.activity{queued}`.
664
- *
665
- * Both `Context.sendActivity()` and `OutboxContext.forwardActivity()` enqueue
666
- * outbox messages with the same metric attributes (role, queue, activity
667
- * type, attempt), so they share this helper rather than each defining a local
668
- * closure. Retry enqueues (attempt > 0) intentionally do not record a
669
- * second `activitypub.outbox.activity{queued}`; retries are reported as
670
- * `result=retried` from the retry-scheduling site, which has the failure
671
- * context.
672
- * @since 2.3.0
673
- */
674
- function recordOutboxEnqueue(meterProvider, outboxQueue, message) {
675
- const metrics = getFederationMetrics(meterProvider);
676
- metrics.recordQueueTaskEnqueued({
677
- role: "outbox",
678
- queue: outboxQueue,
679
- activityType: message.activityType
680
- }, message.attempt);
681
- if (message.attempt === 0) metrics.recordOutboxActivity("queued", message.activityType);
682
- }
683
- /**
684
- * Records `activitypub.fanout.recipients` with the number of recipient
685
- * inboxes a single fanout produced. The histogram is unitless count
686
- * (one measurement per fanout enqueue). Recipient URLs are deliberately
687
- * not recorded; only the activity type, when known.
688
- * @since 2.3.0
689
- */
690
- function recordFanoutRecipients(meterProvider, recipientCount, activityType) {
691
- getFederationMetrics(meterProvider).recordFanoutRecipients(recipientCount, activityType);
692
- }
693
- /**
694
- * Records one `activitypub.inbox.activity` measurement. The
695
- * `activitypub.processing.result` attribute is always present;
696
- * `activitypub.activity.type` is recorded only when Fedify already knows
697
- * the activity type.
698
- * @since 2.3.0
699
- */
700
- function recordInboxActivity(meterProvider, result, activityType) {
701
- getFederationMetrics(meterProvider).recordInboxActivity(result, activityType);
702
- }
703
- /**
704
- * Records one `activitypub.outbox.activity` measurement. The
705
- * `activitypub.processing.result` attribute is always present;
706
- * `activitypub.activity.type` is recorded only when Fedify already knows
707
- * the activity type (it is always known for outbox lifecycle events).
708
- * @since 2.3.0
709
- */
710
- function recordOutboxActivity(meterProvider, result, activityType) {
711
- getFederationMetrics(meterProvider).recordOutboxActivity(result, activityType);
712
- }
713
- /**
714
- * Records one outbound delivery circuit breaker state transition.
715
- * @since 2.3.0
716
- */
717
- function recordCircuitBreakerStateChange(meterProvider, remoteHost, state) {
718
- getFederationMetrics(meterProvider).recordCircuitBreakerStateChange(remoteHost, state);
719
- }
720
- /**
721
- * Records one measurement on `activitypub.key.lookup` (counter) and
722
- * `activitypub.key.lookup.duration` (histogram) for a public-key lookup.
723
- *
724
- * `activitypub.lookup.kind` is always recorded as `public_key`; the result
725
- * classification, remote host, HTTP status code (when an HTTP response was
726
- * received), and `activitypub.cache.enabled` are recorded as attributes on
727
- * both measurements. Full key URLs and key IDs are deliberately omitted to
728
- * keep cardinality bounded.
729
- * @since 2.3.0
730
- */
731
- function recordKeyLookup(meterProvider, attrs) {
732
- getFederationMetrics(meterProvider).recordKeyLookup(attrs);
733
- }
734
- /**
735
- * Records one measurement each on `activitypub.document.fetch` (counter)
736
- * and `activitypub.document.fetch.duration` (histogram) for one remote
737
- * JSON-LD document loader invocation, with bounded
738
- * `activitypub.lookup.kind` and `activitypub.lookup.result` attributes
739
- * plus the optional remote-host, cache-enabled, and HTTP status-code
740
- * attributes. Counter and histogram are always recorded together so
741
- * aggregate rate and latency views stay in sync.
742
- * @since 2.3.0
743
- */
744
- function recordDocumentFetch(meterProvider, attrs) {
745
- getFederationMetrics(meterProvider).recordDocumentFetch(attrs);
746
- }
747
- /**
748
- * Records one `activitypub.document.cache` measurement, classifying the
749
- * lookup as `hit` (the cache returned an entry) or `miss` (the cache was
750
- * consulted and returned nothing, prompting a delegate fetch).
751
- * @since 2.3.0
752
- */
753
- function recordDocumentCache(meterProvider, attrs) {
754
- getFederationMetrics(meterProvider).recordDocumentCache(attrs);
755
- }
756
- /**
757
- * Records one measurement on `webfinger.handle` (counter) and
758
- * `webfinger.handle.duration` (histogram) for an incoming WebFinger
759
- * request handled by Fedify. Counter and histogram are always recorded
760
- * together, with `webfinger.handle.result` set to one of `resolved`,
761
- * `invalid`, `not_found`, `tombstoned`, or `error`. The queried
762
- * resource string is deliberately excluded; it remains on the
763
- * `webfinger.handle` span for trace-level investigation.
764
- * @since 2.3.0
765
- */
766
- function recordWebFingerHandle(meterProvider, attrs) {
767
- getFederationMetrics(meterProvider).recordWebFingerHandle(attrs);
768
- }
769
- /**
770
- * Records one `activitypub.collection.request` measurement for a
771
- * collection or collection-page request handled by Fedify.
772
- * @since 2.3.0
773
- */
774
- function recordCollectionRequest(meterProvider, attrs) {
775
- getFederationMetrics(meterProvider).recordCollectionRequest(attrs);
776
- }
777
- /**
778
- * Records one `activitypub.collection.dispatch.duration` measurement for a
779
- * collection dispatcher callback invocation.
780
- * @since 2.3.0
781
- */
782
- function recordCollectionDispatchDuration(meterProvider, durationMs, attrs) {
783
- getFederationMetrics(meterProvider).recordCollectionDispatchDuration(durationMs, attrs);
784
- }
785
- /**
786
- * Records one `activitypub.collection.page.items` measurement when Fedify
787
- * has materialized collection items in memory.
788
- * @since 2.3.0
789
- */
790
- function recordCollectionPageItems(meterProvider, itemCount, attrs) {
791
- getFederationMetrics(meterProvider).recordCollectionPageItems(itemCount, attrs);
792
- }
793
- /**
794
- * Records one `activitypub.collection.total_items` measurement when a
795
- * collection counter has already reported a total item count.
796
- * @since 2.3.0
797
- */
798
- function recordCollectionTotalItems(meterProvider, totalItems, attrs) {
799
- getFederationMetrics(meterProvider).recordCollectionTotalItems(totalItems, attrs);
800
- }
801
- /**
802
- * Classifies a thrown value from a key or document fetch into the bounded
803
- * {@link LookupResult} taxonomy and, when an HTTP response was received,
804
- * surfaces its status code.
805
- *
806
- * - `FetchError` with a `Response` whose status is `404` or `410`:
807
- * `result=not_found` and the response status code.
808
- * - `FetchError` with any other `Response`: `result=error` and the
809
- * response status code.
810
- * - `FetchError` without a `Response`: `result=network_error`.
811
- * - An `AbortError` (typically from a cancelled fetch): `result=network_error`.
812
- * - A bare `TypeError` (the shape native `fetch()` raises on DNS, connect,
813
- * and TLS failures before any response is observed):
814
- * `result=network_error`.
815
- * - Any other value: `result=error`.
816
- * @since 2.3.0
817
- */
818
- function classifyFetchError(error) {
819
- if (error instanceof _fedify_vocab_runtime.FetchError) {
820
- if (error.response != null) {
821
- const status = error.response.status;
822
- return {
823
- result: status === 404 || status === 410 ? "not_found" : "error",
824
- statusCode: status
825
- };
826
- }
827
- return { result: "network_error" };
828
- }
829
- if (isAbortError$1(error)) return { result: "network_error" };
830
- if (error instanceof TypeError) return { result: "network_error" };
831
- return { result: "error" };
832
- }
833
- /**
834
- * Wraps a {@link DocumentLoader} so each invocation records one
835
- * measurement on `activitypub.document.fetch` (counter) and one on
836
- * `activitypub.document.fetch.duration` (histogram), classifying the
837
- * outcome via {@link classifyFetchError} when the wrapped loader throws
838
- * and as `fetched` on success. The wrapper rethrows whatever the
839
- * wrapped loader throws so caller behavior is unchanged.
840
- *
841
- * The wrapper records the host of the requested URL, including any
842
- * non-default port, on `activitypub.remote.host` when the URL parses; full
843
- * URLs, paths, and query strings are deliberately excluded to keep
844
- * cardinality bounded.
845
- * HTTP status codes are recorded only when the failure carries a
846
- * `Response` (currently, when the wrapped loader throws a
847
- * {@link FetchError} with a non-`null` `response`).
848
- * @since 2.3.0
849
- */
850
- function instrumentDocumentLoader(loader, options) {
851
- const meterProvider = options.meterProvider;
852
- if (meterProvider == null) return loader;
853
- return async (url, opts) => {
854
- const start = performance.now();
855
- let remoteUrl;
856
- try {
857
- remoteUrl = new URL(url);
858
- } catch {
859
- remoteUrl = void 0;
860
- }
861
- try {
862
- const result = await loader(url, opts);
863
- recordDocumentFetch(meterProvider, {
864
- durationMs: getDurationMs(start),
865
- kind: options.kind,
866
- result: "fetched",
867
- remoteUrl,
868
- cacheEnabled: options.cacheEnabled
869
- });
870
- return result;
871
- } catch (error) {
872
- const classified = classifyFetchError(error);
873
- recordDocumentFetch(meterProvider, {
874
- durationMs: getDurationMs(start),
875
- kind: options.kind,
876
- result: classified.result,
877
- remoteUrl,
878
- cacheEnabled: options.cacheEnabled,
879
- statusCode: classified.statusCode
880
- });
881
- throw error;
882
- }
883
- };
884
- }
885
- /**
886
- * Times an awaited public key fetch and records exactly one
887
- * `activitypub.signature.key_fetch.duration` measurement, classifying the
888
- * outcome as `hit`, `fetched`, or `error` based on the `cached` flag and
889
- * whether the returned key is non-null. Errors thrown by the fetch are
890
- * reported as `error` and rethrown, so verifier behavior is unchanged.
891
- *
892
- * Shared by the three signature verifiers (HTTP, Linked Data, Object
893
- * Integrity Proofs); the only per-call variation is the
894
- * `activitypub.signature.kind` attribute value.
895
- * @since 2.3.0
896
- */
897
- async function measureSignatureKeyFetch(meterProvider, kind, fetch) {
898
- const start = performance.now();
899
- try {
900
- const result = await fetch();
901
- getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, result.key != null ? result.cached ? "hit" : "fetched" : "error");
902
- return result;
903
- } catch (error) {
904
- getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, "error");
905
- throw error;
906
- }
907
- }
908
- /**
909
- * Whether the given thrown value is an `AbortError`.
910
- *
911
- * `processQueuedTask` distinguishes aborted tasks (recorded as
912
- * `fedify.queue.task.result=aborted`) from other failures so that backend
913
- * shutdown signals do not inflate the `fedify.queue.task.failed` counter.
914
- * @since 2.3.0
915
- */
916
- function isAbortError$1(error) {
917
- if (error == null || typeof error !== "object") return false;
918
- const name = error.name;
919
- return typeof name === "string" && name === "AbortError";
920
- }
921
- const KNOWN_HTTP_METHODS = new Set([
922
- "CONNECT",
923
- "DELETE",
924
- "GET",
925
- "HEAD",
926
- "OPTIONS",
927
- "PATCH",
928
- "POST",
929
- "PUT",
930
- "QUERY",
931
- "TRACE"
932
- ]);
933
- function normalizeHttpMethod(method) {
934
- const upper = method.toUpperCase();
935
- return KNOWN_HTTP_METHODS.has(upper) ? upper : "_OTHER";
936
- }
937
- const federationMetrics = /* @__PURE__ */ new WeakMap();
938
- /**
939
- * Gets the cached Fedify metric instruments for a meter provider.
940
- * @since 2.3.0
941
- */
942
- function getFederationMetrics(meterProvider = _opentelemetry_api.metrics.getMeterProvider()) {
943
- let instruments = federationMetrics.get(meterProvider);
944
- if (instruments == null) {
945
- instruments = new FederationMetrics(meterProvider);
946
- federationMetrics.set(meterProvider, instruments);
947
- }
948
- return instruments;
949
- }
950
- /**
951
- * Gets the bounded remote host attribute value for a URL.
952
- * @since 2.3.0
953
- */
954
- function getRemoteHost(url) {
955
- return url.host;
956
- }
957
- /**
958
- * Gets an elapsed duration in milliseconds from a `performance.now()` value.
959
- * @since 2.3.0
960
- */
961
- function getDurationMs(start) {
962
- return Math.max(0, performance.now() - start);
963
- }
964
- //#endregion
965
- //#region src/sig/key.ts
966
- /**
967
- * Checks if the given key is valid and supported. No-op if the key is valid,
968
- * otherwise throws an error.
969
- * @param key The key to check.
970
- * @param type Which type of key to check. If not specified, the key can be
971
- * either public or private.
972
- * @throws {TypeError} If the key is invalid or unsupported.
973
- */
974
- function validateCryptoKey(key, type) {
975
- if (type != null && key.type !== type) throw new TypeError(`The key is not a ${type} key.`);
976
- if (!key.extractable) throw new TypeError("The key is not extractable.");
977
- if (key.algorithm.name !== "RSASSA-PKCS1-v1_5" && key.algorithm.name !== "Ed25519") throw new TypeError("Currently only RSASSA-PKCS1-v1_5 and Ed25519 keys are supported. More algorithms will be added in the future!");
978
- if (key.algorithm.name === "RSASSA-PKCS1-v1_5") {
979
- if (key.algorithm.hash.name !== "SHA-256") throw new TypeError("For compatibility with the existing Fediverse software (e.g., Mastodon), hash algorithm for RSASSA-PKCS1-v1_5 keys must be SHA-256.");
980
- }
981
- }
982
- /**
983
- * Generates a key pair which is appropriate for Fedify.
984
- * @param algorithm The algorithm to use. Currently only RSASSA-PKCS1-v1_5 and
985
- * Ed25519 are supported.
986
- * @returns The generated key pair.
987
- * @throws {TypeError} If the algorithm is unsupported.
988
- */
989
- function generateCryptoKeyPair(algorithm) {
990
- if (algorithm == null) (0, _logtape_logtape.getLogger)([
991
- "fedify",
992
- "sig",
993
- "key"
994
- ]).warn("No algorithm specified. Using RSASSA-PKCS1-v1_5 by default, but it is recommended to specify the algorithm explicitly as the parameter will be required in the future.");
995
- if (algorithm == null || algorithm === "RSASSA-PKCS1-v1_5") return crypto.subtle.generateKey({
996
- name: "RSASSA-PKCS1-v1_5",
997
- modulusLength: 4096,
998
- publicExponent: new Uint8Array([
999
- 1,
1000
- 0,
1001
- 1
1002
- ]),
1003
- hash: "SHA-256"
1004
- }, true, ["sign", "verify"]);
1005
- else if (algorithm === "Ed25519") return crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
1006
- throw new TypeError("Unsupported algorithm: " + algorithm);
1007
- }
1008
- /**
1009
- * Exports a key in JWK format.
1010
- * @param key The key to export. Either public or private key.
1011
- * @returns The exported key in JWK format. The key is suitable for
1012
- * serialization and storage.
1013
- * @throws {TypeError} If the key is invalid or unsupported.
1014
- */
1015
- async function exportJwk(key) {
1016
- validateCryptoKey(key);
1017
- const jwk = await crypto.subtle.exportKey("jwk", key);
1018
- if (jwk.crv === "Ed25519") jwk.alg = "Ed25519";
1019
- return jwk;
1020
- }
1021
- /**
1022
- * Imports a key from JWK format.
1023
- * @param jwk The key in JWK format.
1024
- * @param type Which type of key to import, either `"public"` or `"private"`.
1025
- * @returns The imported key.
1026
- * @throws {TypeError} If the key is invalid or unsupported.
1027
- */
1028
- async function importJwk(jwk, type) {
1029
- let key;
1030
- if (jwk.kty === "RSA" && jwk.alg === "RS256") key = await crypto.subtle.importKey("jwk", jwk, {
1031
- name: "RSASSA-PKCS1-v1_5",
1032
- hash: "SHA-256"
1033
- }, true, type === "public" ? ["verify"] : ["sign"]);
1034
- else if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
1035
- if (navigator?.userAgent === "Cloudflare-Workers") {
1036
- jwk = { ...jwk };
1037
- delete jwk.alg;
1038
- }
1039
- key = await crypto.subtle.importKey("jwk", jwk, "Ed25519", true, type === "public" ? ["verify"] : ["sign"]);
1040
- } else throw new TypeError("Unsupported JWK format.");
1041
- validateCryptoKey(key, type);
1042
- return key;
1043
- }
1044
- async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1045
- tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
1046
- return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
1047
- kind: _opentelemetry_api.SpanKind.CLIENT,
1048
- attributes: {
1049
- "http.method": "GET",
1050
- "url.full": keyId.href,
1051
- "url.scheme": keyId.protocol.replace(/:$/, ""),
1052
- "url.domain": keyId.hostname,
1053
- "url.path": keyId.pathname,
1054
- "url.query": keyId.search.replace(/^\?/, ""),
1055
- "url.fragment": keyId.hash.replace(/^#/, "")
1056
- }
1057
- }, async (span) => {
1058
- try {
1059
- const result = await fetcher();
1060
- span.setAttribute("activitypub.actor.key.cached", result.cached);
1061
- return result;
1062
- } catch (e) {
1063
- span.setStatus({
1064
- code: _opentelemetry_api.SpanStatusCode.ERROR,
1065
- message: String(e)
1066
- });
1067
- throw e;
1068
- } finally {
1069
- span.end();
1070
- }
1071
- });
1072
- }
1073
- /**
1074
- * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL.
1075
- * If the given URL contains an {@link Actor} object, it tries to find
1076
- * the corresponding key in the `publicKey` or `assertionMethod` property.
1077
- * @template T The type of the key to fetch. Either {@link CryptographicKey}
1078
- * or {@link Multikey}.
1079
- * @param keyId The URL of the key.
1080
- * @param cls The class of the key to fetch. Either {@link CryptographicKey}
1081
- * or {@link Multikey}.
1082
- * @param options Options for fetching the key. See {@link FetchKeyOptions}.
1083
- * @returns The fetched key or `null` if the key is not found.
1084
- * @since 1.3.0
1085
- */
1086
- function fetchKey(keyId, cls, options = {}) {
1087
- keyId = typeof keyId === "string" ? new URL(keyId) : keyId;
1088
- return withFetchKeySpan(keyId, options.tracerProvider, () => fetchKeyInternal(keyId, cls, options));
1089
- }
1090
- /**
1091
- * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL,
1092
- * preserving transport-level fetch failures for callers that need to inspect
1093
- * why the key could not be loaded.
1094
- *
1095
- * @template T The type of the key to fetch. Either {@link CryptographicKey}
1096
- * or {@link Multikey}.
1097
- * @param keyId The URL of the key.
1098
- * @param cls The class of the key to fetch. Either {@link CryptographicKey}
1099
- * or {@link Multikey}.
1100
- * @param options Options for fetching the key.
1101
- * @returns The fetched key, or detailed fetch failure information.
1102
- * @since 2.1.0
1103
- */
1104
- async function fetchKeyDetailed(keyId, cls, options = {}) {
1105
- const cacheKey = typeof keyId === "string" ? new URL(keyId) : keyId;
1106
- return await withFetchKeySpan(cacheKey, options.tracerProvider, async () => {
1107
- return await fetchKeyWithResult(cacheKey, cls, options, async (cacheKey, keyId, keyCache, logger) => {
1108
- const fetchError = await keyCache?.getFetchError?.(cacheKey);
1109
- if (fetchError != null) {
1110
- logger.debug("Entry {keyId} found in cache with preserved fetch failure details.", { keyId });
1111
- return {
1112
- key: null,
1113
- cached: true,
1114
- fetchError
1115
- };
1116
- }
1117
- logger.debug("Entry {keyId} found in cache, but no fetch failure details are available.", { keyId });
1118
- return {
1119
- key: null,
1120
- cached: true
1121
- };
1122
- }, async (error, cacheKey, keyId, keyCache, logger) => {
1123
- logger.debug("Failed to fetch key {keyId}.", {
1124
- keyId,
1125
- error
1126
- });
1127
- await keyCache?.set(cacheKey, null);
1128
- if (error instanceof _fedify_vocab_runtime.FetchError && error.response != null) {
1129
- const fetchError = {
1130
- status: error.response.status,
1131
- response: error.response.clone()
1132
- };
1133
- await keyCache?.setFetchError?.(cacheKey, fetchError);
1134
- return {
1135
- key: null,
1136
- cached: false,
1137
- fetchError
1138
- };
1139
- }
1140
- const fetchError = { error: error instanceof Error ? error : new Error(String(error)) };
1141
- await keyCache?.setFetchError?.(cacheKey, fetchError);
1142
- return {
1143
- key: null,
1144
- cached: false,
1145
- fetchError
1146
- };
1147
- });
1148
- });
1149
- }
1150
- async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
1151
- if (keyCache == null) return null;
1152
- const cachedKey = await keyCache.get(cacheKey);
1153
- const hit = checkCachedKeyHit(cachedKey, keyId, cls, logger);
1154
- if (hit != null) return hit;
1155
- if (cachedKey === null) {
1156
- logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId });
1157
- return {
1158
- key: null,
1159
- cached: true
1160
- };
1161
- }
1162
- return null;
1163
- }
1164
- function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
1165
- if (cachedKey instanceof cls && cachedKey.publicKey != null) {
1166
- logger.debug("Key {keyId} found in cache.", { keyId });
1167
- return {
1168
- key: cachedKey,
1169
- cached: true
1170
- };
1171
- }
1172
- return null;
1173
- }
1174
- async function clearFetchErrorMetadata(keyId, keyCache) {
1175
- await keyCache?.setFetchError?.(keyId, null);
1176
- }
1177
- function isDidKeyUrl(keyId) {
1178
- return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
1179
- }
1180
- async function doRawKeysMatch(left, right) {
1181
- const [leftRaw, rightRaw] = await Promise.all([crypto.subtle.exportKey("raw", left), crypto.subtle.exportKey("raw", right)]);
1182
- if (leftRaw.byteLength !== rightRaw.byteLength) return false;
1183
- const leftBytes = new Uint8Array(leftRaw);
1184
- const rightBytes = new Uint8Array(rightRaw);
1185
- return leftBytes.every((byte, index) => byte === rightBytes[index]);
1186
- }
1187
- async function isCachedDidKeyValid(key, verificationMethod) {
1188
- try {
1189
- return key.id?.href === verificationMethod.id.href && key.controllerId?.href === verificationMethod.controller.href && await doRawKeysMatch(key.publicKey, verificationMethod.publicKey);
1190
- } catch {
1191
- return false;
1192
- }
1193
- }
1194
- async function resolveDidKey(cacheKey, cls, keyCache, logger) {
1195
- if (!isDidKeyUrl(cacheKey)) return null;
1196
- const keyId = cacheKey.href;
1197
- if (!(cls === _fedify_vocab.Multikey)) {
1198
- logger.debug("Failed to resolve did:key {keyId}; did:key verification methods are only supported as Multikey values.", { keyId });
1199
- return {
1200
- key: null,
1201
- cached: false
1202
- };
1203
- }
1204
- let verificationMethod;
1205
- try {
1206
- verificationMethod = await (0, _fedify_vocab_runtime.parseDidKeyVerificationMethod)(cacheKey);
1207
- } catch (error) {
1208
- logger.debug("Failed to resolve did:key verification method {keyId}: {error}", {
1209
- keyId,
1210
- error
1211
- });
1212
- return {
1213
- key: null,
1214
- cached: false
1215
- };
1216
- }
1217
- const hit = checkCachedKeyHit(await keyCache?.get(cacheKey), keyId, cls, logger);
1218
- if (hit?.key instanceof _fedify_vocab.Multikey && await isCachedDidKeyValid(hit.key, verificationMethod)) return hit;
1219
- const key = new _fedify_vocab.Multikey({
1220
- id: verificationMethod.id,
1221
- controller: verificationMethod.controller,
1222
- publicKey: verificationMethod.publicKey
1223
- });
1224
- await keyCache?.set(cacheKey, key);
1225
- await clearFetchErrorMetadata(cacheKey, keyCache);
1226
- logger.debug("Resolved did:key verification method {keyId}.", { keyId });
1227
- return {
1228
- key,
1229
- cached: false
1230
- };
1231
- }
1232
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1233
- let object;
1234
- try {
1235
- object = await _fedify_vocab.Object.fromJsonLd(document, {
1236
- documentLoader,
1237
- contextLoader,
1238
- tracerProvider
1239
- });
1240
- } catch (e) {
1241
- if (!(e instanceof TypeError)) throw e;
1242
- try {
1243
- object = await cls.fromJsonLd(document, {
1244
- documentLoader,
1245
- contextLoader,
1246
- tracerProvider
1247
- });
1248
- } catch (e) {
1249
- if (e instanceof TypeError) {
1250
- logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
1251
- await keyCache?.set(cacheKey, null);
1252
- await clearFetchErrorMetadata(cacheKey, keyCache);
1253
- return {
1254
- key: null,
1255
- cached: false
1256
- };
1257
- }
1258
- throw e;
1259
- }
1260
- }
1261
- let key = null;
1262
- if (object instanceof cls) key = object;
1263
- else if ((0, _fedify_vocab.isActor)(object)) {
1264
- const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
1265
- documentLoader,
1266
- contextLoader,
1267
- suppressError: true,
1268
- tracerProvider
1269
- }) : object.getAssertionMethods({
1270
- documentLoader,
1271
- contextLoader,
1272
- suppressError: true,
1273
- tracerProvider
1274
- });
1275
- let length = 0;
1276
- let lastKey = null;
1277
- try {
1278
- for await (const k of keys) {
1279
- length++;
1280
- lastKey = k;
1281
- if (k.id?.href === keyId) {
1282
- key = k;
1283
- break;
1284
- }
1285
- }
1286
- } catch (e) {
1287
- if (!(e instanceof TypeError)) throw e;
1288
- logger.debug("Failed to verify; a malformed key was encountered while iterating the keys of {keyId}; treating it as a missing key: {error}", {
1289
- keyId,
1290
- error: e
1291
- });
1292
- }
1293
- const keyIdUrl = new URL(keyId);
1294
- if (key == null && keyIdUrl.hash === "" && length === 1) key = lastKey;
1295
- if (key == null) {
1296
- logger.debug("Failed to verify; object {keyId} returned an {actorType}, but has no key matching {keyId}.", {
1297
- keyId,
1298
- actorType: object.constructor.name
1299
- });
1300
- await keyCache?.set(cacheKey, null);
1301
- await clearFetchErrorMetadata(cacheKey, keyCache);
1302
- return {
1303
- key: null,
1304
- cached: false
1305
- };
1306
- }
1307
- } else {
1308
- logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
1309
- await keyCache?.set(cacheKey, null);
1310
- await clearFetchErrorMetadata(cacheKey, keyCache);
1311
- return {
1312
- key: null,
1313
- cached: false
1314
- };
1315
- }
1316
- if (key.publicKey == null) {
1317
- logger.debug("Failed to verify; key {keyId} has no publicKeyPem field.", { keyId });
1318
- await keyCache?.set(cacheKey, null);
1319
- await clearFetchErrorMetadata(cacheKey, keyCache);
1320
- return {
1321
- key: null,
1322
- cached: false
1323
- };
1324
- }
1325
- if (keyCache != null) {
1326
- await keyCache.set(cacheKey, key);
1327
- logger.debug("Key {keyId} cached.", { keyId });
1328
- }
1329
- await clearFetchErrorMetadata(cacheKey, keyCache);
1330
- return {
1331
- key,
1332
- cached: false
1333
- };
1334
- }
1335
- async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
1336
- const start = performance.now();
1337
- let outcome = { result: "error" };
1338
- try {
1339
- const logger = (0, _logtape_logtape.getLogger)([
1340
- "fedify",
1341
- "sig",
1342
- "key"
1343
- ]);
1344
- const keyId = cacheKey.href;
1345
- const keyCache = options.keyCache;
1346
- const didKey = await resolveDidKey(cacheKey, cls, keyCache, logger);
1347
- if (didKey != null) {
1348
- outcome = { result: didKey.key == null ? "invalid" : didKey.cached ? "hit" : "fetched" };
1349
- return didKey;
1350
- }
1351
- const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
1352
- if (cached?.key === null && cached.cached) {
1353
- const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
1354
- outcome = { result: "hit" };
1355
- return cachedUnavailable;
1356
- }
1357
- if (cached != null) {
1358
- outcome = { result: "hit" };
1359
- return cached;
1360
- }
1361
- logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1362
- let document;
1363
- try {
1364
- document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
1365
- } catch (error) {
1366
- const classified = classifyFetchError(error);
1367
- const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1368
- outcome = classified;
1369
- return errored;
1370
- }
1371
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1372
- outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1373
- return resolved;
1374
- } finally {
1375
- recordKeyLookup(options.meterProvider, {
1376
- durationMs: getDurationMs(start),
1377
- result: outcome.result,
1378
- remoteUrl: cacheKey,
1379
- cacheEnabled: options.keyCache != null,
1380
- statusCode: outcome.statusCode
1381
- });
1382
- }
1383
- }
1384
- async function fetchKeyInternal(keyId, cls, options = {}) {
1385
- return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
1386
- return {
1387
- key: null,
1388
- cached: true
1389
- };
1390
- }, async (error, cacheKey, keyId, keyCache, logger) => {
1391
- logger.debug("Failed to fetch key {keyId}.", {
1392
- keyId,
1393
- error
1394
- });
1395
- await keyCache?.set(cacheKey, null);
1396
- if (error instanceof _fedify_vocab_runtime.FetchError && error.response != null) await keyCache?.setFetchError?.(cacheKey, {
1397
- status: error.response.status,
1398
- response: error.response.clone()
1399
- });
1400
- else await keyCache?.setFetchError?.(cacheKey, { error: error instanceof Error ? error : new Error(String(error)) });
1401
- return {
1402
- key: null,
1403
- cached: false
1404
- };
1405
- });
1406
- }
1407
- //#endregion
1408
- //#region src/sig/http.ts
1409
- const Temporal = require_temporal_polyfill.Temporal;
1410
- const DEFAULT_MAX_REDIRECTION = 20;
1411
- const DOUBLE_KNOCK_TRANSPORT_RETRY_DELAY_MS = 100;
1412
- /**
1413
- * Signs a request using the given private key.
1414
- * @param request The request to sign.
1415
- * @param privateKey The private key to use for signing.
1416
- * @param keyId The key ID to use for the signature. It will be used by the
1417
- * verifier.
1418
- * @returns The signed request.
1419
- * @throws {TypeError} If the private key is invalid or unsupported.
1420
- */
1421
- async function signRequest(request, privateKey, keyId, options = {}) {
1422
- validateCryptoKey(privateKey, "private");
1423
- return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("http_signatures.sign", async (span) => {
1424
- try {
1425
- const spec = options.spec ?? "draft-cavage-http-signatures-12";
1426
- let signed;
1427
- if (spec === "rfc9421") signed = await signRequestRfc9421(request, privateKey, keyId, span, options.currentTime, options.body, options.rfc9421);
1428
- else signed = await signRequestDraft(request, privateKey, keyId, span, options.currentTime, options.body);
1429
- if (span.isRecording()) {
1430
- span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD, signed.method);
1431
- span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_FULL, signed.url);
1432
- for (const [name, value] of signed.headers) span.setAttribute((0, _opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_HEADER)(name), value);
1433
- span.setAttribute("http_signatures.key_id", keyId.href);
1434
- }
1435
- return signed;
1436
- } catch (error) {
1437
- span.setStatus({
1438
- code: _opentelemetry_api.SpanStatusCode.ERROR,
1439
- message: String(error)
1440
- });
1441
- throw error;
1442
- } finally {
1443
- span.end();
1444
- }
1445
- });
1446
- }
1447
- async function signRequestDraft(request, privateKey, keyId, span, currentTime, bodyBuffer) {
1448
- if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
1449
- const url = new URL(request.url);
1450
- const body = bodyBuffer !== void 0 ? bodyBuffer : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1451
- const headers = new Headers(request.headers);
1452
- if (!headers.has("Host")) headers.set("Host", url.host);
1453
- if (!headers.has("Digest") && body != null) {
1454
- const digest = await crypto.subtle.digest("SHA-256", body);
1455
- headers.set("Digest", `SHA-256=${(0, byte_encodings_base64.encodeBase64)(digest)}`);
1456
- if (span.isRecording()) span.setAttribute("http_signatures.digest.sha-256", (0, byte_encodings_hex.encodeHex)(digest));
1457
- }
1458
- if (!headers.has("Date")) headers.set("Date", currentTime == null ? (/* @__PURE__ */ new Date()).toUTCString() : new Date(currentTime.toString()).toUTCString());
1459
- const serialized = [["(request-target)", `${request.method.toLowerCase()} ${url.pathname}`], ...headers];
1460
- const headerNames = serialized.map(([name]) => name);
1461
- const message = serialized.map(([name, value]) => `${name}: ${value.trim()}`).join("\n");
1462
- const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(message));
1463
- const sigHeader = `keyId="${keyId.href}",algorithm="rsa-sha256",headers="${headerNames.join(" ")}",signature="${(0, byte_encodings_base64.encodeBase64)(signature)}"`;
1464
- headers.set("Signature", sigHeader);
1465
- if (span.isRecording()) {
1466
- span.setAttribute("http_signatures.algorithm", "rsa-sha256");
1467
- span.setAttribute("http_signatures.signature", (0, byte_encodings_hex.encodeHex)(signature));
1468
- }
1469
- return new Request(request, {
1470
- headers,
1471
- body
1472
- });
1473
- }
1474
- function formatRfc9421SignatureParameters(params) {
1475
- return Array.from(iterRfc9421(params)).join(";");
1476
- }
1477
- function* iterRfc9421(params) {
1478
- yield `alg="${params.algorithm}"`;
1479
- yield `keyid="${params.keyId.href}"`;
1480
- yield `created=${params.created}`;
1481
- if (params.expires != null) yield `expires=${params.expires}`;
1482
- if (params.nonce != null) yield `nonce="${escapeSfString(params.nonce)}"`;
1483
- if (params.tag != null) yield `tag="${escapeSfString(params.tag)}"`;
1484
- }
1485
- const escapeSfString = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1486
- function formatComponentId(component) {
1487
- return (0, structured_field_values.encodeItem)(new structured_field_values.Item(component.value, component.params));
1488
- }
1489
- /**
1490
- * Creates a signature base for a request according to RFC 9421.
1491
- * @param request The request to create a signature base for.
1492
- * @param components The components to include in the signature base.
1493
- * @param parameters The signature parameters to include in the signature base.
1494
- * @returns The signature base as a string.
1495
- */
1496
- function createRfc9421SignatureBase(request, components, parameters) {
1497
- const url = new URL(request.url);
1498
- return components.map((component) => {
1499
- const id = formatComponentId(component);
1500
- const derived = derivedComponents[component.value]?.(request, url);
1501
- if (derived != null) return `${id}: ${derived}`;
1502
- if (component.value.startsWith("@")) throw new Error(`Unsupported derived component: ${component.value}`);
1503
- const header = request.headers.get(component.value);
1504
- if (header == null) throw new Error(`Missing header: ${component.value}`);
1505
- return `${id}: ${header}`;
1506
- }).concat([`"@signature-params": (${components.map((c) => formatComponentId(c)).join(" ")});${parameters}`]).join("\n");
1507
- }
1508
- const derivedComponents = {
1509
- "@method": (request) => request.method.toUpperCase(),
1510
- "@target-uri": (_, url) => url.href,
1511
- "@authority": (_, url) => url.host,
1512
- "@scheme": (_, url) => url.protocol.slice(0, -1),
1513
- "@request-target": (request, url) => `${request.method.toLowerCase()} ${url.pathname}${url.search}`,
1514
- "@path": (_, url) => url.pathname,
1515
- "@query": (_, { search }) => search.startsWith("?") ? search.slice(1) : search,
1516
- "@query-param": () => {
1517
- throw new Error("@query-param requires a parameter name");
1518
- },
1519
- "@status": () => {
1520
- throw new Error("@status is only valid for responses");
1521
- }
1522
- };
1523
- /**
1524
- * Formats a signature using rfc9421 format.
1525
- * @param signature The raw signature bytes.
1526
- * @param components The components that were signed.
1527
- * @param parameters The signature parameters.
1528
- * @returns The formatted signature string.
1529
- */
1530
- function formatRfc9421Signature(signature, components, parameters, label = "sig1") {
1531
- return [`${label}=(${components.map((c) => formatComponentId(c)).join(" ")});${parameters}`, `${label}=:${(0, byte_encodings_base64.encodeBase64)(signature)}:`];
1532
- }
1533
- /**
1534
- * Parse RFC 9421 Signature-Input header.
1535
- * @param signatureInput The Signature-Input header value.
1536
- * @returns Parsed signature input parameters.
1537
- */
1538
- function parseRfc9421SignatureInput(signatureInput) {
1539
- let dict;
1540
- try {
1541
- dict = (0, structured_field_values.decodeDict)(signatureInput);
1542
- } catch (error) {
1543
- (0, _logtape_logtape.getLogger)([
1544
- "fedify",
1545
- "sig",
1546
- "http"
1547
- ]).debug("Failed to parse Signature-Input header: {signatureInput}", {
1548
- signatureInput,
1549
- error
1550
- });
1551
- return {};
1552
- }
1553
- const result = {};
1554
- for (const [label, item] of Object.entries(dict)) {
1555
- if (!Array.isArray(item.value) || typeof item.params.keyid !== "string" || typeof item.params.created !== "number") continue;
1556
- const components = item.value.filter((subitem) => typeof subitem.value === "string").map((subitem) => ({
1557
- value: subitem.value,
1558
- params: subitem.params ?? {}
1559
- }));
1560
- const params = (0, structured_field_values.encodeItem)(new structured_field_values.Item(0, item.params));
1561
- result[label] = {
1562
- keyId: item.params.keyid,
1563
- alg: item.params.alg,
1564
- created: item.params.created,
1565
- nonce: typeof item.params.nonce === "string" ? item.params.nonce : void 0,
1566
- tag: typeof item.params.tag === "string" ? item.params.tag : void 0,
1567
- components,
1568
- parameters: params.slice(params.indexOf(";") + 1)
1569
- };
1570
- }
1571
- return result;
1572
- }
1573
- /**
1574
- * Parse RFC 9421 Signature header.
1575
- * @param signature The Signature header value.
1576
- * @returns Parsed signature values.
1577
- */
1578
- function parseRfc9421Signature(signature) {
1579
- let dict;
1580
- try {
1581
- dict = (0, structured_field_values.decodeDict)(signature);
1582
- } catch (error) {
1583
- (0, _logtape_logtape.getLogger)([
1584
- "fedify",
1585
- "sig",
1586
- "http"
1587
- ]).debug("Failed to parse Signature header: {signature}", {
1588
- signature,
1589
- error
1590
- });
1591
- return {};
1592
- }
1593
- const result = {};
1594
- for (const [key, value] of Object.entries(dict)) if (value.value instanceof Uint8Array) result[key] = value.value;
1595
- return result;
1596
- }
1597
- async function signRequestRfc9421(request, privateKey, keyId, span, currentTime, bodyBuffer, rfc9421Options) {
1598
- if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
1599
- const url = new URL(request.url);
1600
- const body = bodyBuffer !== void 0 ? bodyBuffer : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1601
- const headers = new Headers(request.headers);
1602
- if (!headers.has("Host")) headers.set("Host", url.host);
1603
- if (!headers.has("Content-Digest") && body != null) {
1604
- const digest = await crypto.subtle.digest("SHA-256", body);
1605
- headers.set("Content-Digest", `sha-256=:${(0, byte_encodings_base64.encodeBase64)(digest)}:`);
1606
- if (span.isRecording()) span.setAttribute("http_signatures.digest.sha-256", (0, byte_encodings_hex.encodeHex)(digest));
1607
- }
1608
- currentTime ??= Temporal.Now.instant();
1609
- const created = currentTime.epochMilliseconds / 1e3 | 0;
1610
- if (!headers.has("Date")) headers.set("Date", new Date(currentTime.toString()).toUTCString());
1611
- const label = rfc9421Options?.label ?? "sig1";
1612
- const components = [...rfc9421Options?.components ?? [
1613
- {
1614
- value: "@method",
1615
- params: {}
1616
- },
1617
- {
1618
- value: "@target-uri",
1619
- params: {}
1620
- },
1621
- {
1622
- value: "@authority",
1623
- params: {}
1624
- },
1625
- {
1626
- value: "host",
1627
- params: {}
1628
- },
1629
- {
1630
- value: "date",
1631
- params: {}
1632
- }
1633
- ], ...body != null ? [{
1634
- value: "content-digest",
1635
- params: {}
1636
- }] : []];
1637
- const signatureParams = formatRfc9421SignatureParameters({
1638
- algorithm: "rsa-v1_5-sha256",
1639
- keyId,
1640
- created,
1641
- expires: rfc9421Options?.expires === true ? (currentTime.epochMilliseconds / 1e3 | 0) + 3600 : void 0,
1642
- nonce: rfc9421Options?.nonce,
1643
- tag: rfc9421Options?.tag
1644
- });
1645
- let signatureBase;
1646
- try {
1647
- signatureBase = createRfc9421SignatureBase(new Request(request.url, {
1648
- method: request.method,
1649
- headers
1650
- }), components, signatureParams);
1651
- } catch (error) {
1652
- throw new TypeError(`Failed to create signature base: ${String(error)}; it is probably a bug in the implementation. Please report it at Fedify's issue tracker.`);
1653
- }
1654
- const signatureBytes = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(signatureBase));
1655
- const [signatureInput, signature] = formatRfc9421Signature(signatureBytes, components, signatureParams, label);
1656
- const existingInput = headers.get("Signature-Input");
1657
- headers.set("Signature-Input", existingInput != null ? `${existingInput}, ${signatureInput}` : signatureInput);
1658
- const existingSignature = headers.get("Signature");
1659
- headers.set("Signature", existingSignature != null ? `${existingSignature}, ${signature}` : signature);
1660
- if (span.isRecording()) {
1661
- span.setAttribute("http_signatures.algorithm", "rsa-v1_5-sha256");
1662
- span.setAttribute("http_signatures.signature", (0, byte_encodings_hex.encodeHex)(signatureBytes));
1663
- span.setAttribute("http_signatures.created", created.toString());
1664
- }
1665
- return new Request(request, {
1666
- headers,
1667
- body
1668
- });
1669
- }
1670
- const supportedHashAlgorithms = {
1671
- "sha": "SHA-1",
1672
- "sha-256": "SHA-256",
1673
- "sha-512": "SHA-512"
1674
- };
1675
- function noSignatureResult() {
1676
- return {
1677
- verified: false,
1678
- reason: { type: "noSignature" }
1679
- };
1680
- }
1681
- function invalidSignatureResult(keyId) {
1682
- return keyId == null ? {
1683
- verified: false,
1684
- reason: { type: "invalidSignature" }
1685
- } : {
1686
- verified: false,
1687
- reason: {
1688
- type: "invalidSignature",
1689
- keyId
1690
- }
1691
- };
1692
- }
1693
- function keyFetchErrorResult(keyId, result) {
1694
- return {
1695
- verified: false,
1696
- reason: {
1697
- type: "keyFetchError",
1698
- keyId,
1699
- result
1700
- }
1701
- };
1702
- }
1703
- function parseKeyId(value) {
1704
- if (value == null) return null;
1705
- try {
1706
- return new URL(value);
1707
- } catch {
1708
- return null;
1709
- }
1710
- }
1711
- function getKeyFetchErrorName(error) {
1712
- return error.name || error.constructor.name || "Error";
1713
- }
1714
- /**
1715
- * Known draft-cavage `algorithm` parameter values, used to keep the
1716
- * `http_signatures.algorithm` metric attribute on a bounded set. The header
1717
- * field is attacker-controlled and not used to select the verification
1718
- * algorithm, so unknown values are dropped from the metric to prevent
1719
- * cardinality blow-up.
1720
- */
1721
- const DRAFT_KNOWN_ALGORITHMS = new Set([
1722
- "ecdsa-sha256",
1723
- "ecdsa-sha384",
1724
- "ecdsa-sha512",
1725
- "ed25519",
1726
- "hs2019",
1727
- "rsa-sha1",
1728
- "rsa-sha256",
1729
- "rsa-sha512"
1730
- ]);
1731
- function classifyHttpVerifyResult(result) {
1732
- if (result.verified) return "verified";
1733
- return result.reason.type === "noSignature" ? "missing" : "rejected";
1734
- }
1735
- function recordVerificationResult(span, result) {
1736
- span.setAttribute("http_signatures.verified", result.verified);
1737
- if (result.verified === true) return;
1738
- const reason = result.reason;
1739
- span.setAttribute("http_signatures.failure_reason", reason.type);
1740
- if ("keyId" in reason && reason.keyId != null) span.setAttribute("http_signatures.key_id", reason.keyId.href);
1741
- if (reason.type !== "keyFetchError") return;
1742
- if ("status" in reason.result) span.setAttribute("http_signatures.key_fetch_status", reason.result.status);
1743
- else span.setAttribute("http_signatures.key_fetch_error", getKeyFetchErrorName(reason.result.error));
1744
- }
1745
- /**
1746
- * Verifies the signature of a request.
1747
- *
1748
- * Note that this function consumes the request body, so it should not be used
1749
- * if the request body is already consumed. Consuming the request body after
1750
- * calling this function is okay, since this function clones the request
1751
- * under the hood.
1752
- *
1753
- * @param request The request to verify.
1754
- * @param options Options for verifying the request.
1755
- * @returns The public key of the verified signature, or `null` if the signature
1756
- * could not be verified.
1757
- */
1758
- async function verifyRequest(request, options = {}) {
1759
- const result = await verifyRequestDetailed(request, options);
1760
- return result.verified ? result.key : null;
1761
- }
1762
- /**
1763
- * Verifies the signature of a request and returns a structured failure reason
1764
- * when verification does not succeed.
1765
- *
1766
- * @param request The request to verify.
1767
- * @param options Options for verifying the request.
1768
- * @returns The verified public key, or a structured verification failure.
1769
- * @since 2.1.0
1770
- */
1771
- async function verifyRequestDetailed(request, options = {}) {
1772
- return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("http_signatures.verify", async (span) => {
1773
- if (span.isRecording()) {
1774
- span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD, request.method);
1775
- span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_FULL, request.url);
1776
- for (const [name, value] of request.headers) span.setAttribute((0, _opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_HEADER)(name), value);
1777
- }
1778
- const start = performance.now();
1779
- const metricsContext = {};
1780
- let result;
1781
- let threw = false;
1782
- try {
1783
- let spec = options.spec;
1784
- if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
1785
- if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
1786
- else result = await verifyRequestDraft(request, span, metricsContext, options);
1787
- recordVerificationResult(span, result);
1788
- if (!result.verified) span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1789
- return result;
1790
- } catch (error) {
1791
- threw = true;
1792
- span.setStatus({
1793
- code: _opentelemetry_api.SpanStatusCode.ERROR,
1794
- message: String(error)
1795
- });
1796
- throw error;
1797
- } finally {
1798
- const classified = threw ? "error" : classifyHttpVerifyResult(result);
1799
- const failureReason = result != null && !result.verified && result.reason.type !== "noSignature" ? result.reason.type : void 0;
1800
- getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "http", classified, {
1801
- algorithm: metricsContext.algorithm,
1802
- failureReason
1803
- });
1804
- span.end();
1805
- }
1806
- });
1807
- }
1808
- async function verifyRequestDraft(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
1809
- const logger = (0, _logtape_logtape.getLogger)([
1810
- "fedify",
1811
- "sig",
1812
- "http"
1813
- ]);
1814
- if (request.bodyUsed) {
1815
- logger.error("Failed to verify; the request body is already consumed.", { url: request.url });
1816
- return noSignatureResult();
1817
- } else if (request.body?.locked) {
1818
- logger.error("Failed to verify; the request body is locked.", { url: request.url });
1819
- return noSignatureResult();
1820
- }
1821
- const originalRequest = request;
1822
- request = request.clone();
1823
- const sigHeader = request.headers.get("Signature");
1824
- if (sigHeader == null) {
1825
- logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
1826
- return noSignatureResult();
1827
- }
1828
- const sigValues = Object.fromEntries(sigHeader.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)=(?:"([^"]*)"|(\d+))\s*$/)).filter((m) => m != null).map((m) => [m[1], m[2] ?? m[3]]));
1829
- const parsedKeyId = parseKeyId(sigValues.keyId);
1830
- const dateHeader = request.headers.get("Date");
1831
- if (dateHeader == null) {
1832
- logger.debug("Failed to verify; no Date header found.", { headers: Object.fromEntries(request.headers.entries()) });
1833
- return invalidSignatureResult(parsedKeyId);
1834
- }
1835
- const digestHeader = request.headers.get("Digest");
1836
- if (request.method !== "GET" && request.method !== "HEAD" && digestHeader == null) {
1837
- logger.debug("Failed to verify; no Digest header found.", { headers: Object.fromEntries(request.headers.entries()) });
1838
- return invalidSignatureResult(parsedKeyId);
1839
- }
1840
- let body = null;
1841
- if (digestHeader != null) {
1842
- body = await request.arrayBuffer();
1843
- const digests = digestHeader.split(",").map((pair) => pair.includes("=") ? pair.split("=", 2) : [pair, ""]);
1844
- let matched = false;
1845
- for (let [algo, digestBase64] of digests) {
1846
- algo = algo.trim().toLowerCase();
1847
- if (!(algo in supportedHashAlgorithms)) continue;
1848
- let digest;
1849
- try {
1850
- digest = (0, byte_encodings_base64.decodeBase64)(digestBase64);
1851
- } catch (error) {
1852
- logger.debug("Failed to verify; invalid base64 encoding: {digest}.", {
1853
- digest: digestBase64,
1854
- error
1855
- });
1856
- return invalidSignatureResult(parsedKeyId);
1857
- }
1858
- if (span.isRecording()) span.setAttribute(`http_signatures.digest.${algo}`, (0, byte_encodings_hex.encodeHex)(digest));
1859
- const expectedDigest = await crypto.subtle.digest(supportedHashAlgorithms[algo], body);
1860
- if (!timingSafeEqual(digest, new Uint8Array(expectedDigest))) {
1861
- logger.debug("Failed to verify; digest mismatch ({algorithm}): {digest} != {expectedDigest}.", {
1862
- algorithm: algo,
1863
- digest: digestBase64,
1864
- expectedDigest: (0, byte_encodings_base64.encodeBase64)(expectedDigest)
1865
- });
1866
- return invalidSignatureResult(parsedKeyId);
1867
- }
1868
- matched = true;
1869
- }
1870
- if (!matched) {
1871
- logger.debug("Failed to verify; no supported digest algorithm found. Supported: {supportedAlgorithms}; found: {algorithms}.", {
1872
- supportedAlgorithms: Object.keys(supportedHashAlgorithms),
1873
- algorithms: digests.map(([algo]) => algo)
1874
- });
1875
- return invalidSignatureResult(parsedKeyId);
1876
- }
1877
- }
1878
- const date = Temporal.Instant.from(new Date(dateHeader).toISOString());
1879
- const now = currentTime ?? Temporal.Now.instant();
1880
- if (timeWindow !== false) {
1881
- const tw = timeWindow ?? { hours: 1 };
1882
- if (Temporal.Instant.compare(date, now.add(tw)) > 0) {
1883
- logger.debug("Failed to verify; Date is too far in the future.", {
1884
- date: date.toString(),
1885
- now: now.toString()
1886
- });
1887
- return invalidSignatureResult(parsedKeyId);
1888
- } else if (Temporal.Instant.compare(date, now.subtract(tw)) < 0) {
1889
- logger.debug("Failed to verify; Date is too far in the past.", {
1890
- date: date.toString(),
1891
- now: now.toString()
1892
- });
1893
- return invalidSignatureResult(parsedKeyId);
1894
- }
1895
- }
1896
- if (!("keyId" in sigValues)) {
1897
- logger.debug("Failed to verify; no keyId field found in the Signature header.", { signature: sigHeader });
1898
- return invalidSignatureResult(null);
1899
- } else if (!("headers" in sigValues)) {
1900
- logger.debug("Failed to verify; no headers field found in the Signature header.", { signature: sigHeader });
1901
- return invalidSignatureResult(parsedKeyId);
1902
- } else if (!("signature" in sigValues)) {
1903
- logger.debug("Failed to verify; no signature field found in the Signature header.", { signature: sigHeader });
1904
- return invalidSignatureResult(parsedKeyId);
1905
- }
1906
- if ("expires" in sigValues) {
1907
- const expiresSeconds = parseInt(sigValues.expires);
1908
- if (!Number.isInteger(expiresSeconds)) {
1909
- logger.debug("Failed to verify; invalid expires field in the Signature header: {expires}.", {
1910
- expires: sigValues.expires,
1911
- signature: sigHeader
1912
- });
1913
- return invalidSignatureResult(parsedKeyId);
1914
- }
1915
- const expires = Temporal.Instant.fromEpochMilliseconds(expiresSeconds * 1e3);
1916
- if (Temporal.Instant.compare(now, expires) > 0) {
1917
- logger.debug("Failed to verify; signature expired at {expires} (now: {now}).", {
1918
- expires: expires.toString(),
1919
- now: now.toString(),
1920
- signature: sigHeader
1921
- });
1922
- return invalidSignatureResult(parsedKeyId);
1923
- }
1924
- }
1925
- if ("created" in sigValues) {
1926
- const createdSeconds = parseInt(sigValues.created);
1927
- if (!Number.isInteger(createdSeconds)) {
1928
- logger.debug("Failed to verify; invalid created field in the Signature header: {created}.", {
1929
- created: sigValues.created,
1930
- signature: sigHeader
1931
- });
1932
- return invalidSignatureResult(parsedKeyId);
1933
- }
1934
- if (timeWindow !== false) {
1935
- const created = Temporal.Instant.fromEpochMilliseconds(createdSeconds * 1e3);
1936
- const tw = timeWindow ?? { minutes: 1 };
1937
- if (Temporal.Instant.compare(created, now.add(tw)) > 0) {
1938
- logger.debug("Failed to verify; created is too far in the future.", {
1939
- created: created.toString(),
1940
- now: now.toString()
1941
- });
1942
- return invalidSignatureResult(parsedKeyId);
1943
- } else if (Temporal.Instant.compare(created, now.subtract(tw)) < 0) {
1944
- logger.debug("Failed to verify; created is too far in the past.", {
1945
- created: created.toString(),
1946
- now: now.toString()
1947
- });
1948
- return invalidSignatureResult(parsedKeyId);
1949
- }
1950
- }
1951
- }
1952
- const { keyId, headers, signature } = sigValues;
1953
- const keyIdUrl = parseKeyId(keyId);
1954
- if (keyIdUrl == null) return invalidSignatureResult(null);
1955
- span?.setAttribute("http_signatures.key_id", keyId);
1956
- if ("algorithm" in sigValues) {
1957
- span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
1958
- const normalizedAlgorithm = sigValues.algorithm.toLowerCase();
1959
- if (DRAFT_KNOWN_ALGORITHMS.has(normalizedAlgorithm)) metricsContext.algorithm = normalizedAlgorithm;
1960
- }
1961
- const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyIdUrl, _fedify_vocab.CryptographicKey, {
1962
- documentLoader,
1963
- contextLoader,
1964
- keyCache,
1965
- tracerProvider,
1966
- meterProvider
1967
- }));
1968
- if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
1969
- if (key == null) return invalidSignatureResult(keyIdUrl);
1970
- const headerNames = headers.split(/\s+/g);
1971
- if (!headerNames.includes("(request-target)") || !headerNames.includes("date")) {
1972
- logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers });
1973
- return invalidSignatureResult(keyIdUrl);
1974
- }
1975
- if (body != null && !headerNames.includes("digest")) {
1976
- logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers });
1977
- return invalidSignatureResult(keyIdUrl);
1978
- }
1979
- const message = headerNames.map((name) => `${name}: ` + (name === "(request-target)" ? `${request.method.toLowerCase()} ${new URL(request.url).pathname}` : name === "(created)" ? sigValues.created ?? "" : name === "(expires)" ? sigValues.expires ?? "" : name === "host" ? request.headers.get("host") ?? new URL(request.url).host : request.headers.get(name))).join("\n");
1980
- const sig = (0, byte_encodings_base64.decodeBase64)(signature);
1981
- span?.setAttribute("http_signatures.signature", (0, byte_encodings_hex.encodeHex)(sig));
1982
- if (!await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, sig, new TextEncoder().encode(message))) {
1983
- if (cached) {
1984
- logger.debug("Failed to verify with the cached key {keyId}; signature {signature} is invalid. Retrying with the freshly fetched key...", {
1985
- keyId,
1986
- signature,
1987
- message
1988
- });
1989
- return await verifyRequestDraft(originalRequest, span, metricsContext, {
1990
- documentLoader,
1991
- contextLoader,
1992
- timeWindow,
1993
- currentTime,
1994
- keyCache: {
1995
- get: () => Promise.resolve(void 0),
1996
- set: async (keyId, key) => await keyCache?.set(keyId, key)
1997
- },
1998
- meterProvider,
1999
- tracerProvider
2000
- });
2001
- }
2002
- logger.debug("Failed to verify with the fetched key {keyId}; signature {signature} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
2003
- keyId,
2004
- signature,
2005
- message
2006
- });
2007
- return invalidSignatureResult(keyIdUrl);
2008
- }
2009
- return {
2010
- verified: true,
2011
- key
2012
- };
2013
- }
2014
- /**
2015
- * RFC 9421 map of algorithm identifiers to WebCrypto algorithms
2016
- */
2017
- const rfc9421AlgorithmMap = {
2018
- "rsa-v1_5-sha256": {
2019
- name: "RSASSA-PKCS1-v1_5",
2020
- hash: "SHA-256"
2021
- },
2022
- "rsa-v1_5-sha512": {
2023
- name: "RSASSA-PKCS1-v1_5",
2024
- hash: "SHA-512"
2025
- },
2026
- "rsa-pss-sha512": {
2027
- name: "RSA-PSS",
2028
- hash: "SHA-512"
2029
- },
2030
- "ecdsa-p256-sha256": {
2031
- name: "ECDSA",
2032
- hash: "SHA-256"
2033
- },
2034
- "ecdsa-p384-sha384": {
2035
- name: "ECDSA",
2036
- hash: "SHA-384"
2037
- },
2038
- "ed25519": { name: "Ed25519" }
2039
- };
2040
- /**
2041
- * Verifies a Content-Digest header according to RFC 9421.
2042
- * @param digestHeader The Content-Digest header value.
2043
- * @param body The message body to verify against.
2044
- * @returns Whether the digest is valid.
2045
- */
2046
- async function verifyRfc9421ContentDigest(digestHeader, body) {
2047
- const digests = digestHeader.split(",").map((pair) => {
2048
- pair = pair.trim();
2049
- const pos = pair.indexOf("=");
2050
- const algo = pos < 0 ? pair : pair.slice(0, pos);
2051
- const value = pos < 0 ? "" : pair.slice(pos + 1);
2052
- return {
2053
- algo: algo.trim().toLowerCase(),
2054
- value: value.trim()
2055
- };
2056
- });
2057
- for (const { algo, value } of digests) {
2058
- let hashAlgo;
2059
- if (algo === "sha-256") hashAlgo = "SHA-256";
2060
- else if (algo === "sha-512") hashAlgo = "SHA-512";
2061
- else continue;
2062
- const base64Match = value.match(/^:([^:]+):$/);
2063
- if (!base64Match) continue;
2064
- let digest;
2065
- try {
2066
- digest = (0, byte_encodings_base64.decodeBase64)(base64Match[1]);
2067
- } catch {
2068
- continue;
2069
- }
2070
- const calculatedDigest = await crypto.subtle.digest(hashAlgo, body);
2071
- if (timingSafeEqual(digest, new Uint8Array(calculatedDigest))) return true;
2072
- }
2073
- return false;
2074
- }
2075
- async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
2076
- const logger = (0, _logtape_logtape.getLogger)([
2077
- "fedify",
2078
- "sig",
2079
- "http"
2080
- ]);
2081
- if (request.bodyUsed) {
2082
- logger.error("Failed to verify; the request body is already consumed.", { url: request.url });
2083
- return noSignatureResult();
2084
- } else if (request.body?.locked) {
2085
- logger.error("Failed to verify; the request body is locked.", { url: request.url });
2086
- return noSignatureResult();
2087
- }
2088
- const originalRequest = request;
2089
- request = request.clone();
2090
- const signatureInputHeader = request.headers.get("Signature-Input");
2091
- if (!signatureInputHeader) {
2092
- logger.debug("Failed to verify; no Signature-Input header found.", { headers: Object.fromEntries(request.headers.entries()) });
2093
- return noSignatureResult();
2094
- }
2095
- const signatureHeader = request.headers.get("Signature");
2096
- if (!signatureHeader) {
2097
- logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
2098
- return noSignatureResult();
2099
- }
2100
- const signatureInputs = parseRfc9421SignatureInput(signatureInputHeader);
2101
- logger.debug("Parsed Signature-Input header: {signatureInputs}", { signatureInputs });
2102
- const signatures = parseRfc9421Signature(signatureHeader);
2103
- const signatureNames = Object.keys(signatureInputs);
2104
- if (signatureNames.length === 0) {
2105
- logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
2106
- return invalidSignatureResult(null);
2107
- }
2108
- let failure = noSignatureResult();
2109
- let failureAlgorithm;
2110
- const setFailure = (result, algorithm) => {
2111
- failure = result;
2112
- failureAlgorithm = algorithm;
2113
- };
2114
- for (const sigName of signatureNames) {
2115
- if (!signatures[sigName]) {
2116
- setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
2117
- continue;
2118
- }
2119
- const sigInput = signatureInputs[sigName];
2120
- const sigBytes = signatures[sigName];
2121
- const keyId = parseKeyId(sigInput.keyId);
2122
- if (!sigInput.keyId) {
2123
- logger.debug("Failed to verify; missing keyId in signature {signatureName}.", {
2124
- signatureName: sigName,
2125
- signatureInput: signatureInputHeader
2126
- });
2127
- setFailure(invalidSignatureResult(null));
2128
- continue;
2129
- }
2130
- if (!sigInput.created) {
2131
- logger.debug("Failed to verify; missing created timestamp in signature {signatureName}.", {
2132
- signatureName: sigName,
2133
- signatureInput: signatureInputHeader
2134
- });
2135
- setFailure(invalidSignatureResult(keyId));
2136
- continue;
2137
- }
2138
- const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3);
2139
- const now = currentTime ?? Temporal.Now.instant();
2140
- if (timeWindow !== false) {
2141
- const tw = timeWindow ?? { hours: 1 };
2142
- if (Temporal.Instant.compare(signatureCreated, now.add(tw)) > 0) {
2143
- logger.debug("Failed to verify; signature created time is too far in the future.", {
2144
- created: signatureCreated.toString(),
2145
- now: now.toString()
2146
- });
2147
- setFailure(invalidSignatureResult(keyId));
2148
- continue;
2149
- } else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) {
2150
- logger.debug("Failed to verify; signature created time is too far in the past.", {
2151
- created: signatureCreated.toString(),
2152
- now: now.toString()
2153
- });
2154
- setFailure(invalidSignatureResult(keyId));
2155
- continue;
2156
- }
2157
- }
2158
- if (request.method !== "GET" && request.method !== "HEAD" && sigInput.components.some((c) => c.value === "content-digest")) {
2159
- const contentDigestHeader = request.headers.get("Content-Digest");
2160
- if (!contentDigestHeader) {
2161
- logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
2162
- setFailure(invalidSignatureResult(keyId));
2163
- continue;
2164
- }
2165
- if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
2166
- logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
2167
- setFailure(invalidSignatureResult(keyId));
2168
- continue;
2169
- }
2170
- }
2171
- span?.setAttribute("http_signatures.key_id", sigInput.keyId);
2172
- span?.setAttribute("http_signatures.created", sigInput.created.toString());
2173
- if (keyId == null) {
2174
- setFailure(invalidSignatureResult(null));
2175
- continue;
2176
- }
2177
- const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, _fedify_vocab.CryptographicKey, {
2178
- documentLoader,
2179
- contextLoader,
2180
- keyCache,
2181
- tracerProvider,
2182
- meterProvider
2183
- }));
2184
- if (fetchError != null) {
2185
- setFailure(keyFetchErrorResult(keyId, fetchError));
2186
- continue;
2187
- }
2188
- if (!key) {
2189
- logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
2190
- setFailure(invalidSignatureResult(keyId));
2191
- continue;
2192
- }
2193
- let alg = sigInput.alg?.toLowerCase();
2194
- if (alg == null) {
2195
- if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
2196
- else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
2197
- else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
2198
- else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
2199
- }
2200
- if (alg) span?.setAttribute("http_signatures.algorithm", alg);
2201
- const algorithm = alg && rfc9421AlgorithmMap[alg];
2202
- const candidateAlgorithm = algorithm ? alg : void 0;
2203
- if (!algorithm) {
2204
- logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
2205
- algorithm: sigInput.alg,
2206
- supported: Object.keys(rfc9421AlgorithmMap)
2207
- });
2208
- setFailure(invalidSignatureResult(keyId));
2209
- continue;
2210
- }
2211
- let signatureBase;
2212
- try {
2213
- signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
2214
- } catch (error) {
2215
- logger.debug("Failed to create signature base for verification: {error}", {
2216
- error,
2217
- signatureInput: sigInput
2218
- });
2219
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
2220
- continue;
2221
- }
2222
- const signatureBaseBytes = new TextEncoder().encode(signatureBase);
2223
- span?.setAttribute("http_signatures.signature", (0, byte_encodings_hex.encodeHex)(sigBytes));
2224
- try {
2225
- if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
2226
- metricsContext.algorithm = candidateAlgorithm;
2227
- return {
2228
- verified: true,
2229
- key,
2230
- signatureLabel: sigName
2231
- };
2232
- } else if (cached) {
2233
- logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
2234
- return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
2235
- documentLoader,
2236
- contextLoader,
2237
- timeWindow,
2238
- currentTime,
2239
- keyCache: {
2240
- get: () => Promise.resolve(void 0),
2241
- set: async (keyId, key) => await keyCache?.set(keyId, key)
2242
- },
2243
- spec: "rfc9421",
2244
- meterProvider,
2245
- tracerProvider
2246
- });
2247
- } else {
2248
- logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
2249
- keyId: sigInput.keyId,
2250
- signatureBase
2251
- });
2252
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
2253
- }
2254
- } catch (error) {
2255
- logger.debug("Error during signature verification: {error}", {
2256
- error,
2257
- keyId: sigInput.keyId,
2258
- algorithm: sigInput.alg
2259
- });
2260
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
2261
- }
2262
- }
2263
- metricsContext.algorithm = failureAlgorithm;
2264
- return failure;
2265
- }
2266
- /**
2267
- * Helper function to create a new Request for redirect handling.
2268
- * @param request The original request.
2269
- * @param location The redirect location.
2270
- * @param body The request body as ArrayBuffer or null.
2271
- * @returns A new Request object for the redirect.
2272
- */
2273
- function createRedirectRequest(request, location, body) {
2274
- const url = new URL(location, request.url);
2275
- return new Request(url, {
2276
- method: request.method,
2277
- headers: request.headers,
2278
- body,
2279
- redirect: "manual",
2280
- signal: request.signal,
2281
- mode: request.mode,
2282
- credentials: request.credentials,
2283
- referrer: request.referrer,
2284
- referrerPolicy: request.referrerPolicy,
2285
- integrity: request.integrity,
2286
- keepalive: request.keepalive,
2287
- cache: request.cache
2288
- });
2289
- }
2290
- async function fetchDoubleKnockRequest(request, signedRequest, signal) {
2291
- const maxAttempts = request.method === "GET" || request.method === "HEAD" ? 2 : 1;
2292
- for (let attempt = 1;; attempt++) try {
2293
- return await fetch(signedRequest, {
2294
- redirect: "manual",
2295
- signal
2296
- });
2297
- } catch (error) {
2298
- const abortedSignal = getAbortedSignal(signal, request.signal, signedRequest.signal);
2299
- if (abortedSignal != null) throw getAbortReason(abortedSignal);
2300
- if (isAbortError(error)) throw error;
2301
- if (attempt >= maxAttempts) throw createFetchError(request.url, error);
2302
- await sleep(DOUBLE_KNOCK_TRANSPORT_RETRY_DELAY_MS, signal, request.signal, signedRequest.signal);
2303
- }
2304
- }
2305
- function createFetchError(url, cause) {
2306
- const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
2307
- error.cause = cause;
2308
- return error;
2309
- }
2310
- function isAbortError(error) {
2311
- return error instanceof Error && error.name === "AbortError";
2312
- }
2313
- async function sleep(ms, ...signals) {
2314
- const abortSignals = signals.filter((signal) => signal != null);
2315
- const abortedSignal = getAbortedSignal(...abortSignals);
2316
- if (abortedSignal != null) throw getAbortReason(abortedSignal);
2317
- if (abortSignals.length < 1) {
2318
- await new Promise((resolve) => setTimeout(resolve, ms));
2319
- return;
2320
- }
2321
- await new Promise((resolve, reject) => {
2322
- const removeAbortListeners = () => {
2323
- for (const signal of abortSignals) signal.removeEventListener("abort", handleAbort);
2324
- };
2325
- const timeout = setTimeout(() => {
2326
- removeAbortListeners();
2327
- resolve();
2328
- }, ms);
2329
- function handleAbort(event) {
2330
- clearTimeout(timeout);
2331
- removeAbortListeners();
2332
- reject(getAbortReason(event.currentTarget));
2333
- }
2334
- for (const signal of abortSignals) signal.addEventListener("abort", handleAbort, { once: true });
2335
- });
2336
- }
2337
- function getAbortedSignal(...signals) {
2338
- return signals.find((signal) => signal?.aborted);
2339
- }
2340
- function getAbortReason(signal) {
2341
- return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
2342
- }
2343
- /**
2344
- * Performs a double-knock request to the given URL. For the details of
2345
- * double-knocking, see
2346
- * <https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions>.
2347
- * @param request The request to send.
2348
- * @param identity The identity to use for signing the request.
2349
- * @param options The options for double-knock requests.
2350
- * @returns The response to the request.
2351
- * @since 1.6.0
2352
- */
2353
- async function doubleKnock(request, identity, options = {}) {
2354
- return await doubleKnockInternal(request, identity, options);
2355
- }
2356
- async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
2357
- const { specDeterminer, log, tracerProvider, signal } = options;
2358
- const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
2359
- visited.add(request.url);
2360
- const origin = new URL(request.url).origin;
2361
- const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
2362
- const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
2363
- let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
2364
- spec: firstTrySpec,
2365
- tracerProvider,
2366
- body
2367
- });
2368
- log?.(signedRequest);
2369
- let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
2370
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
2371
- if (redirected >= maximumRedirection) throw new _fedify_vocab_runtime.FetchError(request.url, `Too many redirections (${redirected + 1})`);
2372
- const redirectRequest = createRedirectRequest(request, response.headers.get("Location"), body);
2373
- if (visited.has(redirectRequest.url)) throw new _fedify_vocab_runtime.FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
2374
- return doubleKnockInternal(redirectRequest, identity, {
2375
- ...options,
2376
- body
2377
- }, redirected + 1, visited);
2378
- } else if (response.status === 400 || response.status === 401 || response.status > 401) {
2379
- const logger = (0, _logtape_logtape.getLogger)([
2380
- "fedify",
2381
- "sig",
2382
- "http"
2383
- ]);
2384
- const acceptSigHeader = response.headers.get("Accept-Signature");
2385
- if (acceptSigHeader != null) {
2386
- const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
2387
- const localKeyId = identity.keyId.href;
2388
- const localAlg = "rsa-v1_5-sha256";
2389
- let fulfilled = false;
2390
- let challengeRequest;
2391
- for (const entry of entries) {
2392
- const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
2393
- if (rfc9421 == null) continue;
2394
- logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
2395
- label: rfc9421.label,
2396
- components: rfc9421.components
2397
- });
2398
- try {
2399
- challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
2400
- spec: "rfc9421",
2401
- tracerProvider,
2402
- body,
2403
- rfc9421
2404
- });
2405
- fulfilled = true;
2406
- } catch (error) {
2407
- logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
2408
- label: entry.label,
2409
- error
2410
- });
2411
- }
2412
- }
2413
- if (fulfilled && challengeRequest != null) {
2414
- signedRequest = challengeRequest;
2415
- log?.(signedRequest);
2416
- response = await fetch(signedRequest, {
2417
- redirect: "manual",
2418
- signal
2419
- });
2420
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) return doubleKnock(createRedirectRequest(request, response.headers.get("Location"), body), identity, {
2421
- ...options,
2422
- body
2423
- });
2424
- }
2425
- if (fulfilled && response.status < 300) {
2426
- await specDeterminer?.rememberSpec(origin, "rfc9421");
2427
- return response;
2428
- }
2429
- if (fulfilled && response.status !== 400 && response.status !== 401) return response;
2430
- }
2431
- const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
2432
- logger.debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
2433
- spec: firstTrySpec,
2434
- secondSpec: spec,
2435
- status: response.status,
2436
- statusText: response.statusText
2437
- });
2438
- signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
2439
- spec,
2440
- tracerProvider,
2441
- body
2442
- });
2443
- log?.(signedRequest);
2444
- response = await fetchDoubleKnockRequest(request, signedRequest, signal);
2445
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
2446
- if (redirected >= maximumRedirection) throw new _fedify_vocab_runtime.FetchError(request.url, `Too many redirections (${redirected + 1})`);
2447
- const redirectRequest = createRedirectRequest(request, response.headers.get("Location"), body);
2448
- if (visited.has(redirectRequest.url)) throw new _fedify_vocab_runtime.FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
2449
- return doubleKnockInternal(redirectRequest, identity, {
2450
- ...options,
2451
- body
2452
- }, redirected + 1, visited);
2453
- } else if (response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
2454
- } else await specDeterminer?.rememberSpec(origin, firstTrySpec);
2455
- return response;
2456
- }
2457
- /**
2458
- * Performs a timing-safe equality comparison between two `Uint8Array` values.
2459
- *
2460
- * This function is designed to take a constant amount of time to execute,
2461
- * dependent only on the length of the longer of the two arrays,
2462
- * regardless of where the first difference in bytes occurs. This helps
2463
- * prevent timing attacks.
2464
- *
2465
- * @param a The first bytes.
2466
- * @param b The second bytes.
2467
- * @returns `true` if the arrays are of the same length and contain the same
2468
- * bytes, `false` otherwise.
2469
- * @since 1.6.0
2470
- */
2471
- function timingSafeEqual(a, b) {
2472
- const lenA = a.length;
2473
- const lenB = b.length;
2474
- const commonLength = Math.max(lenA, lenB);
2475
- let result = 0;
2476
- for (let i = 0; i < commonLength; i++) {
2477
- const byteA = i < lenA ? a[i] : 0;
2478
- const byteB = i < lenB ? b[i] : 0;
2479
- result |= byteA ^ byteB;
2480
- }
2481
- result |= lenA ^ lenB;
2482
- return result === 0;
2483
- }
2484
- //#endregion
2485
- Object.defineProperty(exports, "doubleKnock", {
2486
- enumerable: true,
2487
- get: function() {
2488
- return doubleKnock;
2489
- }
2490
- });
2491
- Object.defineProperty(exports, "exportJwk", {
2492
- enumerable: true,
2493
- get: function() {
2494
- return exportJwk;
2495
- }
2496
- });
2497
- Object.defineProperty(exports, "fetchKey", {
2498
- enumerable: true,
2499
- get: function() {
2500
- return fetchKey;
2501
- }
2502
- });
2503
- Object.defineProperty(exports, "fetchKeyDetailed", {
2504
- enumerable: true,
2505
- get: function() {
2506
- return fetchKeyDetailed;
2507
- }
2508
- });
2509
- Object.defineProperty(exports, "formatAcceptSignature", {
2510
- enumerable: true,
2511
- get: function() {
2512
- return formatAcceptSignature;
2513
- }
2514
- });
2515
- Object.defineProperty(exports, "fulfillAcceptSignature", {
2516
- enumerable: true,
2517
- get: function() {
2518
- return fulfillAcceptSignature;
2519
- }
2520
- });
2521
- Object.defineProperty(exports, "generateCryptoKeyPair", {
2522
- enumerable: true,
2523
- get: function() {
2524
- return generateCryptoKeyPair;
2525
- }
2526
- });
2527
- Object.defineProperty(exports, "getDurationMs", {
2528
- enumerable: true,
2529
- get: function() {
2530
- return getDurationMs;
2531
- }
2532
- });
2533
- Object.defineProperty(exports, "getFederationMetrics", {
2534
- enumerable: true,
2535
- get: function() {
2536
- return getFederationMetrics;
2537
- }
2538
- });
2539
- Object.defineProperty(exports, "getRemoteHost", {
2540
- enumerable: true,
2541
- get: function() {
2542
- return getRemoteHost;
2543
- }
2544
- });
2545
- Object.defineProperty(exports, "importJwk", {
2546
- enumerable: true,
2547
- get: function() {
2548
- return importJwk;
2549
- }
2550
- });
2551
- Object.defineProperty(exports, "instrumentDocumentLoader", {
2552
- enumerable: true,
2553
- get: function() {
2554
- return instrumentDocumentLoader;
2555
- }
2556
- });
2557
- Object.defineProperty(exports, "isAbortError", {
2558
- enumerable: true,
2559
- get: function() {
2560
- return isAbortError$1;
2561
- }
2562
- });
2563
- Object.defineProperty(exports, "measureSignatureKeyFetch", {
2564
- enumerable: true,
2565
- get: function() {
2566
- return measureSignatureKeyFetch;
2567
- }
2568
- });
2569
- Object.defineProperty(exports, "name", {
2570
- enumerable: true,
2571
- get: function() {
2572
- return name;
2573
- }
2574
- });
2575
- Object.defineProperty(exports, "parseAcceptSignature", {
2576
- enumerable: true,
2577
- get: function() {
2578
- return parseAcceptSignature;
2579
- }
2580
- });
2581
- Object.defineProperty(exports, "parseRfc9421SignatureInput", {
2582
- enumerable: true,
2583
- get: function() {
2584
- return parseRfc9421SignatureInput;
2585
- }
2586
- });
2587
- Object.defineProperty(exports, "recordCircuitBreakerStateChange", {
2588
- enumerable: true,
2589
- get: function() {
2590
- return recordCircuitBreakerStateChange;
2591
- }
2592
- });
2593
- Object.defineProperty(exports, "recordCollectionDispatchDuration", {
2594
- enumerable: true,
2595
- get: function() {
2596
- return recordCollectionDispatchDuration;
2597
- }
2598
- });
2599
- Object.defineProperty(exports, "recordCollectionPageItems", {
2600
- enumerable: true,
2601
- get: function() {
2602
- return recordCollectionPageItems;
2603
- }
2604
- });
2605
- Object.defineProperty(exports, "recordCollectionRequest", {
2606
- enumerable: true,
2607
- get: function() {
2608
- return recordCollectionRequest;
2609
- }
2610
- });
2611
- Object.defineProperty(exports, "recordCollectionTotalItems", {
2612
- enumerable: true,
2613
- get: function() {
2614
- return recordCollectionTotalItems;
2615
- }
2616
- });
2617
- Object.defineProperty(exports, "recordDocumentCache", {
2618
- enumerable: true,
2619
- get: function() {
2620
- return recordDocumentCache;
2621
- }
2622
- });
2623
- Object.defineProperty(exports, "recordFanoutRecipients", {
2624
- enumerable: true,
2625
- get: function() {
2626
- return recordFanoutRecipients;
2627
- }
2628
- });
2629
- Object.defineProperty(exports, "recordInboxActivity", {
2630
- enumerable: true,
2631
- get: function() {
2632
- return recordInboxActivity;
2633
- }
2634
- });
2635
- Object.defineProperty(exports, "recordOutboxActivity", {
2636
- enumerable: true,
2637
- get: function() {
2638
- return recordOutboxActivity;
2639
- }
2640
- });
2641
- Object.defineProperty(exports, "recordOutboxEnqueue", {
2642
- enumerable: true,
2643
- get: function() {
2644
- return recordOutboxEnqueue;
2645
- }
2646
- });
2647
- Object.defineProperty(exports, "recordWebFingerHandle", {
2648
- enumerable: true,
2649
- get: function() {
2650
- return recordWebFingerHandle;
2651
- }
2652
- });
2653
- Object.defineProperty(exports, "registerQueueDepthGauge", {
2654
- enumerable: true,
2655
- get: function() {
2656
- return registerQueueDepthGauge;
2657
- }
2658
- });
2659
- Object.defineProperty(exports, "signRequest", {
2660
- enumerable: true,
2661
- get: function() {
2662
- return signRequest;
2663
- }
2664
- });
2665
- Object.defineProperty(exports, "validateAcceptSignature", {
2666
- enumerable: true,
2667
- get: function() {
2668
- return validateAcceptSignature;
2669
- }
2670
- });
2671
- Object.defineProperty(exports, "validateCryptoKey", {
2672
- enumerable: true,
2673
- get: function() {
2674
- return validateCryptoKey;
2675
- }
2676
- });
2677
- Object.defineProperty(exports, "verifyRequest", {
2678
- enumerable: true,
2679
- get: function() {
2680
- return verifyRequest;
2681
- }
2682
- });
2683
- Object.defineProperty(exports, "verifyRequestDetailed", {
2684
- enumerable: true,
2685
- get: function() {
2686
- return verifyRequestDetailed;
2687
- }
2688
- });
2689
- Object.defineProperty(exports, "version", {
2690
- enumerable: true,
2691
- get: function() {
2692
- return version;
2693
- }
2694
- });