@fedify/fedify 2.1.0-dev.565 → 2.1.0-dev.592

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 (96) hide show
  1. package/dist/accept-D7sAxyNa.js +143 -0
  2. package/dist/{assert_rejects-Ce45JcFg.js → assert_rejects-0h7I2Esa.js} +1 -1
  3. package/dist/{builder-Deoi2N2z.js → builder-B24i8eYp.js} +3 -3
  4. package/dist/compat/mod.d.cts +3 -3
  5. package/dist/compat/mod.d.ts +3 -3
  6. package/dist/compat/transformers.test.js +17 -16
  7. package/dist/{context-DL0cPpPV.d.cts → context-BcqA-0BL.d.cts} +52 -2
  8. package/dist/{context--RwChtri.d.ts → context-DyJjQQ_H.d.ts} +52 -2
  9. package/dist/{deno-CEdy89j9.js → deno-OR506Yti.js} +1 -2
  10. package/dist/{docloader-CL1QPJzN.js → docloader-BG_pP2fW.js} +2 -2
  11. package/dist/federation/builder.test.js +7 -7
  12. package/dist/federation/collection.test.js +5 -5
  13. package/dist/federation/handler.test.js +806 -26
  14. package/dist/federation/idempotency.test.js +22 -21
  15. package/dist/federation/inbox.test.js +3 -3
  16. package/dist/federation/keycache.test.js +1 -1
  17. package/dist/federation/kv.test.js +4 -4
  18. package/dist/federation/middleware.test.js +22 -21
  19. package/dist/federation/mod.cjs +4 -4
  20. package/dist/federation/mod.d.cts +4 -4
  21. package/dist/federation/mod.d.ts +4 -4
  22. package/dist/federation/mod.js +4 -4
  23. package/dist/federation/mq.test.js +4 -4
  24. package/dist/federation/negotiation.test.js +5 -5
  25. package/dist/federation/retry.test.js +2 -2
  26. package/dist/federation/router.test.js +4 -4
  27. package/dist/federation/send.test.js +11 -10
  28. package/dist/federation/webfinger.test.js +22 -21
  29. package/dist/{http-Dm9n1mRe.js → http-BUCxbGks.js} +144 -49
  30. package/dist/{http-DsqqmkXi.d.cts → http-BudnHZE2.d.cts} +229 -1
  31. package/dist/{http-iDlaLy8a.cjs → http-CaXARmaJ.cjs} +307 -50
  32. package/dist/{http-BbfOqHGG.d.ts → http-Dax_FIBo.d.ts} +229 -1
  33. package/dist/{http-VpqmUjje.js → http-DePHjWKP.js} +278 -51
  34. package/dist/{inbox-CMtnW0RE.js → inbox-D_LU1opv.js} +1 -1
  35. package/dist/{key-B0yADkL8.js → key-Cx3Tx_In.js} +1 -1
  36. package/dist/{kv-cache-551Om14-.cjs → kv-cache-CYTDBChd.cjs} +1 -1
  37. package/dist/{kv-cache-BSATpUtX.js → kv-cache-DizRqYX4.js} +1 -1
  38. package/dist/{ld-BBmbv1nb.js → ld-CLMJw_iX.js} +3 -3
  39. package/dist/{middleware-Cx0tTbX1.js → middleware--uATyG9i.js} +95 -18
  40. package/dist/{middleware-DpdPMZII.js → middleware-4fo4pEtA.js} +4 -4
  41. package/dist/{middleware-D11GYoP-.cjs → middleware-9YDezkYJ.cjs} +94 -17
  42. package/dist/middleware-C2PqSUaA.js +27 -0
  43. package/dist/middleware-DNY45l5T.cjs +12 -0
  44. package/dist/{middleware-Cldp2YSv.js → middleware-DzICTgdC.js} +113 -34
  45. package/dist/{mod-DE8MYisy.d.cts → mod-B7QkWzrL.d.cts} +1 -1
  46. package/dist/{mod-DKG0ovjR.d.cts → mod-Bx9jcLB8.d.cts} +1 -1
  47. package/dist/{mod-CFBU2OT3.d.cts → mod-Coe7KEgX.d.cts} +1 -1
  48. package/dist/{mod-BugwI0JN.d.ts → mod-Cs2dYEwI.d.ts} +1 -1
  49. package/dist/{mod-DcfFNgYf.d.ts → mod-D6MdymW7.d.ts} +1 -1
  50. package/dist/{mod-CvxylbuV.d.ts → mod-D6dOd--H.d.ts} +1 -1
  51. package/dist/{mod-Z7lIaCfo.d.ts → mod-SMHOMNpZ.d.ts} +1 -1
  52. package/dist/{mod-Dp0kK0hO.d.cts → mod-em2Il1eD.d.cts} +1 -1
  53. package/dist/mod.cjs +12 -4
  54. package/dist/mod.d.cts +8 -8
  55. package/dist/mod.d.ts +8 -8
  56. package/dist/mod.js +9 -5
  57. package/dist/nodeinfo/client.test.js +4 -4
  58. package/dist/nodeinfo/handler.test.js +22 -21
  59. package/dist/nodeinfo/types.test.js +4 -4
  60. package/dist/otel/exporter.test.js +4 -4
  61. package/dist/{owner-C1ZyG4NL.js → owner-D5J299vd.js} +1 -1
  62. package/dist/{proof-wclcUq0C.js → proof-BBLHhWMC.js} +2 -2
  63. package/dist/{proof-CgK60TcQ.cjs → proof-BVl5IgbN.cjs} +3 -3
  64. package/dist/{proof-DnRq8s8f.js → proof-CiCp_mCG.js} +2 -2
  65. package/dist/{send-DNJyYRVU.js → send-2b0Fn9cn.js} +2 -2
  66. package/dist/sig/accept.test.d.ts +3 -0
  67. package/dist/sig/accept.test.js +451 -0
  68. package/dist/sig/http.test.js +452 -27
  69. package/dist/sig/key.test.js +7 -7
  70. package/dist/sig/ld.test.js +6 -6
  71. package/dist/sig/mod.cjs +6 -2
  72. package/dist/sig/mod.d.cts +3 -3
  73. package/dist/sig/mod.d.ts +3 -3
  74. package/dist/sig/mod.js +3 -3
  75. package/dist/sig/owner.test.js +8 -8
  76. package/dist/sig/proof.test.js +8 -8
  77. package/dist/testing/mod.js +1 -1
  78. package/dist/utils/docloader.test.js +10 -9
  79. package/dist/utils/kv-cache.test.js +1 -1
  80. package/dist/utils/mod.cjs +2 -2
  81. package/dist/utils/mod.d.cts +2 -2
  82. package/dist/utils/mod.d.ts +2 -2
  83. package/dist/utils/mod.js +2 -2
  84. package/package.json +6 -7
  85. package/dist/middleware-BDr0P6dx.cjs +0 -12
  86. package/dist/middleware-BZ8WpBo6.js +0 -26
  87. /package/dist/{assert_not_equals-C80BG-_5.js → assert_not_equals-f3m3epl3.js} +0 -0
  88. /package/dist/{assert_throws-BNXdRGWP.js → assert_throws-rjdMBf31.js} +0 -0
  89. /package/dist/{collection-CcnIw1qY.js → collection-CSzG2j1P.js} +0 -0
  90. /package/dist/{context-pa9aIrwp.js → context-Aqenou7c.js} +0 -0
  91. /package/dist/{keycache-C7k8s1Bk.js → keycache-CpGWAUbj.js} +0 -0
  92. /package/dist/{keys-ZbcByPg9.js → keys-BFve7QQv.js} +0 -0
  93. /package/dist/{kv-cache-El7We5sy.js → kv-cache-Bw2F2ABq.js} +0 -0
  94. /package/dist/{negotiation-5NPJL6zp.js → negotiation-BlAuS_nr.js} +0 -0
  95. /package/dist/{retry-D4GJ670a.js → retry-mqLf4b-R.js} +0 -0
  96. /package/dist/{std__assert-DWivtrGR.js → std__assert-X-_kMxKM.js} +0 -0
@@ -6,14 +6,14 @@ import { getLogger } from "@logtape/logtape";
6
6
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
7
7
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
8
8
  import { encodeHex } from "byte-encodings/hex";
9
+ import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
9
10
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
10
11
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
11
- import { Item, decodeDict, encodeItem } from "structured-field-values";
12
12
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
13
13
 
14
14
  //#region deno.json
15
15
  var name = "@fedify/fedify";
16
- var version = "2.1.0-dev.565+b4d238a9";
16
+ var version = "2.1.0-dev.592+6c1f6e6f";
17
17
  var license = "MIT";
18
18
  var exports = {
19
19
  ".": "./src/mod.ts",
@@ -35,7 +35,6 @@ var imports = {
35
35
  "fetch-mock": "npm:fetch-mock@^12.5.2",
36
36
  "json-canon": "npm:json-canon@^1.0.1",
37
37
  "jsonld": "npm:jsonld@^9.0.0",
38
- "multicodec": "npm:multicodec@^3.2.1",
39
38
  "pkijs": "npm:pkijs@^3.3.3",
40
39
  "structured-field-values": "npm:structured-field-values@^2.0.4",
41
40
  "uri-template-router": "npm:uri-template-router@^1.0.0",
@@ -126,6 +125,140 @@ var deno_default = {
126
125
  tasks
127
126
  };
128
127
 
128
+ //#endregion
129
+ //#region src/sig/accept.ts
130
+ /**
131
+ * Parses an `Accept-Signature` header value (RFC 9421 §5.1) into an
132
+ * array of {@link AcceptSignatureMember} objects.
133
+ *
134
+ * The `Accept-Signature` field is a Dictionary Structured Field
135
+ * (RFC 8941 §3.2). Each dictionary member describes a single
136
+ * requested message signature.
137
+ *
138
+ * On parse failure (malformed or empty header), returns an empty array.
139
+ *
140
+ * @param header The raw `Accept-Signature` header value string.
141
+ * @returns An array of parsed members. Empty if the header is
142
+ * malformed or empty.
143
+ * @since 2.1.0
144
+ */
145
+ function parseAcceptSignature(header) {
146
+ try {
147
+ return parseEachSignature(decodeDict(header));
148
+ } catch {
149
+ getLogger([
150
+ "fedify",
151
+ "sig",
152
+ "http"
153
+ ]).warn("Failed to parse Accept-Signature header: {header}", { header });
154
+ return [];
155
+ }
156
+ }
157
+ const compactObject = (obj) => Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== void 0));
158
+ const parseEachSignature = (dict) => Object.entries(dict).filter(([_, item]) => Array.isArray(item.value)).map(([label, item]) => ({
159
+ label,
160
+ components: item.value.filter((subitem) => typeof subitem.value === "string").map((subitem) => ({
161
+ value: subitem.value,
162
+ params: subitem.params ?? {}
163
+ })),
164
+ parameters: compactParams(item)
165
+ }));
166
+ const compactParams = (item) => {
167
+ const { keyid, alg, created, expires, nonce, tag } = item.params ?? {};
168
+ return compactObject({
169
+ keyid: stringOrUndefined(keyid),
170
+ alg: stringOrUndefined(alg),
171
+ created: trueOrUndefined(created),
172
+ expires: trueOrUndefined(expires),
173
+ nonce: stringOrUndefined(nonce),
174
+ tag: stringOrUndefined(tag)
175
+ });
176
+ };
177
+ const stringOrUndefined = (v) => typeof v === "string" ? v : void 0;
178
+ const trueOrUndefined = (v) => v === true ? true : void 0;
179
+ /**
180
+ * Serializes an array of {@link AcceptSignatureMember} objects into an
181
+ * `Accept-Signature` header value string (RFC 9421 §5.1).
182
+ *
183
+ * The output is a Dictionary Structured Field (RFC 8941 §3.2).
184
+ *
185
+ * @param members The members to serialize.
186
+ * @returns The serialized header value string.
187
+ * @since 2.1.0
188
+ */
189
+ function formatAcceptSignature(members) {
190
+ const items = members.map((member) => [member.label, new Item(compToItems(member), compactParameters(member))]);
191
+ return encodeDict(Object.fromEntries(items));
192
+ }
193
+ const compToItems = (member) => member.components.map((c) => new Item(c.value, c.params));
194
+ const compactParameters = (member) => {
195
+ const { keyid, alg, created, expires, nonce, tag } = member.parameters;
196
+ return compactObject({
197
+ keyid,
198
+ alg,
199
+ created,
200
+ expires,
201
+ nonce,
202
+ tag
203
+ });
204
+ };
205
+ /**
206
+ * Filters out {@link AcceptSignatureMember} entries whose covered
207
+ * components include response-only identifiers (`@status`) that are
208
+ * not applicable to request-target messages, as required by
209
+ * [RFC 9421 §5](https://www.rfc-editor.org/rfc/rfc9421#section-5).
210
+ *
211
+ * A warning is logged for each discarded entry.
212
+ *
213
+ * @param members The parsed `Accept-Signature` entries to validate.
214
+ * @returns Only entries that are valid for request-target messages.
215
+ * @since 2.1.0
216
+ */
217
+ function validateAcceptSignature(members) {
218
+ const logger = getLogger([
219
+ "fedify",
220
+ "sig",
221
+ "http"
222
+ ]);
223
+ return members.filter((member) => {
224
+ if (member.components.every((c) => c.value !== "@status")) return true;
225
+ logLabel(logger, member.label);
226
+ return false;
227
+ });
228
+ }
229
+ const logLabel = (logger, label) => logger.warn("Discarding Accept-Signature member {label}: covered components include response-only identifier @status.", { label });
230
+ /**
231
+ * Attempts to translate an {@link AcceptSignatureMember} challenge into
232
+ * RFC 9421 signing options that the local signer can fulfill.
233
+ *
234
+ * Returns `null` if the challenge cannot be fulfilled—for example, if
235
+ * the requested `alg` or `keyid` is incompatible with the local key.
236
+ *
237
+ * Safety constraints:
238
+ * - `alg`: only honored if it matches `localAlg`.
239
+ * - `keyid`: only honored if it matches `localKeyId`.
240
+ * - `components`: passed through exactly as requested, per RFC 9421 §5.2.
241
+ * - `nonce`, `tag`, and `expires` are passed through directly.
242
+ *
243
+ * @param entry The challenge entry from the `Accept-Signature` header.
244
+ * @param localKeyId The local key identifier (e.g., the actor key URL).
245
+ * @param localAlg The algorithm of the local private key
246
+ * (e.g., `"rsa-v1_5-sha256"`).
247
+ * @returns Signing options if the challenge can be fulfilled, or `null`.
248
+ * @since 2.1.0
249
+ */
250
+ function fulfillAcceptSignature(entry, localKeyId, localAlg) {
251
+ if (entry.parameters.alg != null && entry.parameters.alg !== localAlg) return null;
252
+ if (entry.parameters.keyid != null && entry.parameters.keyid !== localKeyId) return null;
253
+ return {
254
+ label: entry.label,
255
+ components: entry.components,
256
+ nonce: entry.parameters.nonce,
257
+ tag: entry.parameters.tag,
258
+ expires: entry.parameters.expires
259
+ };
260
+ }
261
+
129
262
  //#endregion
130
263
  //#region src/sig/key.ts
131
264
  /**
@@ -493,7 +626,7 @@ async function signRequest(request, privateKey, keyId, options = {}) {
493
626
  try {
494
627
  const spec = options.spec ?? "draft-cavage-http-signatures-12";
495
628
  let signed;
496
- if (spec === "rfc9421") signed = await signRequestRfc9421(request, privateKey, keyId, span, options.currentTime, options.body);
629
+ if (spec === "rfc9421") signed = await signRequestRfc9421(request, privateKey, keyId, span, options.currentTime, options.body, options.rfc9421);
497
630
  else signed = await signRequestDraft(request, privateKey, keyId, span, options.currentTime, options.body);
498
631
  if (span.isRecording()) {
499
632
  span.setAttribute(ATTR_HTTP_REQUEST_METHOD, signed.method);
@@ -541,7 +674,19 @@ async function signRequestDraft(request, privateKey, keyId, span, currentTime, b
541
674
  });
542
675
  }
543
676
  function formatRfc9421SignatureParameters(params) {
544
- return `alg="${params.algorithm}";keyid="${params.keyId.href}";created=${params.created}`;
677
+ return Array.from(iterRfc9421(params)).join(";");
678
+ }
679
+ function* iterRfc9421(params) {
680
+ yield `alg="${params.algorithm}"`;
681
+ yield `keyid="${params.keyId.href}"`;
682
+ yield `created=${params.created}`;
683
+ if (params.expires != null) yield `expires=${params.expires}`;
684
+ if (params.nonce != null) yield `nonce="${escapeSfString(params.nonce)}"`;
685
+ if (params.tag != null) yield `tag="${escapeSfString(params.tag)}"`;
686
+ }
687
+ const escapeSfString = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
688
+ function formatComponentId(component) {
689
+ return encodeItem(new Item(component.value, component.params));
545
690
  }
546
691
  /**
547
692
  * Creates a signature base for a request according to RFC 9421.
@@ -552,30 +697,31 @@ function formatRfc9421SignatureParameters(params) {
552
697
  */
553
698
  function createRfc9421SignatureBase(request, components, parameters) {
554
699
  const url = new URL(request.url);
555
- const baseComponents = [];
556
- for (const component of components) {
557
- let value;
558
- if (component === "@method") value = request.method.toUpperCase();
559
- else if (component === "@target-uri") value = request.url;
560
- else if (component === "@authority") value = url.host;
561
- else if (component === "@scheme") value = url.protocol.slice(0, -1);
562
- else if (component === "@request-target") value = `${request.method.toLowerCase()} ${url.pathname}${url.search}`;
563
- else if (component === "@path") value = url.pathname;
564
- else if (component === "@query") value = url.search.startsWith("?") ? url.search.slice(1) : url.search;
565
- else if (component === "@query-param") throw new Error("@query-param requires a parameter name");
566
- else if (component === "@status") throw new Error("@status is only valid for responses");
567
- else if (component.startsWith("@")) throw new Error(`Unsupported derived component: ${component}`);
568
- else {
569
- const header = request.headers.get(component);
570
- if (header == null) throw new Error(`Missing header: ${component}`);
571
- value = header;
572
- }
573
- baseComponents.push(`"${component}": ${value}`);
574
- }
575
- const sigComponents = components.map((c) => `"${c}"`).join(" ");
576
- baseComponents.push(`"@signature-params": (${sigComponents});${parameters}`);
577
- return baseComponents.join("\n");
700
+ return components.map((component) => {
701
+ const id = formatComponentId(component);
702
+ const derived = derivedComponents[component.value]?.(request, url);
703
+ if (derived != null) return `${id}: ${derived}`;
704
+ if (component.value.startsWith("@")) throw new Error(`Unsupported derived component: ${component.value}`);
705
+ const header = request.headers.get(component.value);
706
+ if (header == null) throw new Error(`Missing header: ${component.value}`);
707
+ return `${id}: ${header}`;
708
+ }).concat([`"@signature-params": (${components.map((c) => formatComponentId(c)).join(" ")});${parameters}`]).join("\n");
578
709
  }
710
+ const derivedComponents = {
711
+ "@method": (request) => request.method.toUpperCase(),
712
+ "@target-uri": (_, url) => url.href,
713
+ "@authority": (_, url) => url.host,
714
+ "@scheme": (_, url) => url.protocol.slice(0, -1),
715
+ "@request-target": (request, url) => `${request.method.toLowerCase()} ${url.pathname}${url.search}`,
716
+ "@path": (_, url) => url.pathname,
717
+ "@query": (_, { search }) => search.startsWith("?") ? search.slice(1) : search,
718
+ "@query-param": () => {
719
+ throw new Error("@query-param requires a parameter name");
720
+ },
721
+ "@status": () => {
722
+ throw new Error("@status is only valid for responses");
723
+ }
724
+ };
579
725
  /**
580
726
  * Formats a signature using rfc9421 format.
581
727
  * @param signature The raw signature bytes.
@@ -583,9 +729,9 @@ function createRfc9421SignatureBase(request, components, parameters) {
583
729
  * @param parameters The signature parameters.
584
730
  * @returns The formatted signature string.
585
731
  */
586
- function formatRfc9421Signature(signature, components, parameters) {
587
- const signatureInputValue = `sig1=("${components.join("\" \"")}");${parameters}`;
588
- const signatureValue = `sig1=:${encodeBase64(signature)}:`;
732
+ function formatRfc9421Signature(signature, components, parameters, label = "sig1") {
733
+ const signatureInputValue = `${label}=(${components.map((c) => formatComponentId(c)).join(" ")});${parameters}`;
734
+ const signatureValue = `${label}=:${encodeBase64(signature)}:`;
589
735
  return [signatureInputValue, signatureValue];
590
736
  }
591
737
  /**
@@ -611,12 +757,17 @@ function parseRfc9421SignatureInput(signatureInput) {
611
757
  const result = {};
612
758
  for (const [label, item] of Object.entries(dict)) {
613
759
  if (!Array.isArray(item.value) || typeof item.params.keyid !== "string" || typeof item.params.created !== "number") continue;
614
- const components = item.value.map((subitem) => subitem.value).filter((v) => typeof v === "string");
760
+ const components = item.value.filter((subitem) => typeof subitem.value === "string").map((subitem) => ({
761
+ value: subitem.value,
762
+ params: subitem.params ?? {}
763
+ }));
615
764
  const params = encodeItem(new Item(0, item.params));
616
765
  result[label] = {
617
766
  keyId: item.params.keyid,
618
767
  alg: item.params.alg,
619
768
  created: item.params.created,
769
+ nonce: typeof item.params.nonce === "string" ? item.params.nonce : void 0,
770
+ tag: typeof item.params.tag === "string" ? item.params.tag : void 0,
620
771
  components,
621
772
  parameters: params.slice(params.indexOf(";") + 1)
622
773
  };
@@ -647,7 +798,7 @@ function parseRfc9421Signature(signature) {
647
798
  for (const [key, value] of Object.entries(dict)) if (value.value instanceof Uint8Array) result[key] = value.value;
648
799
  return result;
649
800
  }
650
- async function signRequestRfc9421(request, privateKey, keyId, span, currentTime, bodyBuffer) {
801
+ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime, bodyBuffer, rfc9421Options) {
651
802
  if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
652
803
  const url = new URL(request.url);
653
804
  const body = bodyBuffer !== void 0 ? bodyBuffer : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
@@ -661,18 +812,40 @@ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime,
661
812
  currentTime ??= Temporal.Now.instant();
662
813
  const created = currentTime.epochMilliseconds / 1e3 | 0;
663
814
  if (!headers.has("Date")) headers.set("Date", new Date(currentTime.toString()).toUTCString());
664
- const components = [
665
- "@method",
666
- "@target-uri",
667
- "@authority",
668
- "host",
669
- "date"
670
- ];
671
- if (body != null) components.push("content-digest");
815
+ const label = rfc9421Options?.label ?? "sig1";
816
+ const components = [...rfc9421Options?.components ?? [
817
+ {
818
+ value: "@method",
819
+ params: {}
820
+ },
821
+ {
822
+ value: "@target-uri",
823
+ params: {}
824
+ },
825
+ {
826
+ value: "@authority",
827
+ params: {}
828
+ },
829
+ {
830
+ value: "host",
831
+ params: {}
832
+ },
833
+ {
834
+ value: "date",
835
+ params: {}
836
+ }
837
+ ], ...body != null ? [{
838
+ value: "content-digest",
839
+ params: {}
840
+ }] : []];
841
+ const expires = rfc9421Options?.expires === true ? (currentTime.epochMilliseconds / 1e3 | 0) + 3600 : void 0;
672
842
  const signatureParams = formatRfc9421SignatureParameters({
673
843
  algorithm: "rsa-v1_5-sha256",
674
844
  keyId,
675
- created
845
+ created,
846
+ expires,
847
+ nonce: rfc9421Options?.nonce,
848
+ tag: rfc9421Options?.tag
676
849
  });
677
850
  let signatureBase;
678
851
  try {
@@ -684,9 +857,11 @@ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime,
684
857
  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.`);
685
858
  }
686
859
  const signatureBytes = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(signatureBase));
687
- const [signatureInput, signature] = formatRfc9421Signature(signatureBytes, components, signatureParams);
688
- headers.set("Signature-Input", signatureInput);
689
- headers.set("Signature", signature);
860
+ const [signatureInput, signature] = formatRfc9421Signature(signatureBytes, components, signatureParams, label);
861
+ const existingInput = headers.get("Signature-Input");
862
+ headers.set("Signature-Input", existingInput != null ? `${existingInput}, ${signatureInput}` : signatureInput);
863
+ const existingSignature = headers.get("Signature");
864
+ headers.set("Signature", existingSignature != null ? `${existingSignature}, ${signature}` : signature);
690
865
  if (span.isRecording()) {
691
866
  span.setAttribute("http_signatures.algorithm", "rsa-v1_5-sha256");
692
867
  span.setAttribute("http_signatures.signature", encodeHex(signatureBytes));
@@ -1145,7 +1320,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1145
1320
  continue;
1146
1321
  }
1147
1322
  }
1148
- if (request.method !== "GET" && request.method !== "HEAD" && sigInput.components.includes("content-digest")) {
1323
+ if (request.method !== "GET" && request.method !== "HEAD" && sigInput.components.some((c) => c.value === "content-digest")) {
1149
1324
  const contentDigestHeader = request.headers.get("Content-Digest");
1150
1325
  if (!contentDigestHeader) {
1151
1326
  logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
@@ -1215,7 +1390,8 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1215
1390
  const verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
1216
1391
  if (verified) return {
1217
1392
  verified: true,
1218
- key
1393
+ key,
1394
+ signatureLabel: sigName
1219
1395
  };
1220
1396
  else if (cached) {
1221
1397
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
@@ -1304,12 +1480,63 @@ async function doubleKnock(request, identity, options = {}) {
1304
1480
  body
1305
1481
  });
1306
1482
  } else if (response.status === 400 || response.status === 401 || response.status > 401) {
1307
- const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1308
- getLogger([
1483
+ const logger = getLogger([
1309
1484
  "fedify",
1310
1485
  "sig",
1311
1486
  "http"
1312
- ]).debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1487
+ ]);
1488
+ const acceptSigHeader = response.headers.get("Accept-Signature");
1489
+ if (acceptSigHeader != null) {
1490
+ const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
1491
+ const localKeyId = identity.keyId.href;
1492
+ const localAlg = "rsa-v1_5-sha256";
1493
+ let fulfilled = false;
1494
+ let challengeRequest;
1495
+ for (const entry of entries) {
1496
+ const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1497
+ if (rfc9421 == null) continue;
1498
+ logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
1499
+ label: rfc9421.label,
1500
+ components: rfc9421.components
1501
+ });
1502
+ try {
1503
+ challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
1504
+ spec: "rfc9421",
1505
+ tracerProvider,
1506
+ body,
1507
+ rfc9421
1508
+ });
1509
+ fulfilled = true;
1510
+ } catch (error) {
1511
+ logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
1512
+ label: entry.label,
1513
+ error
1514
+ });
1515
+ }
1516
+ }
1517
+ if (fulfilled && challengeRequest != null) {
1518
+ signedRequest = challengeRequest;
1519
+ log?.(signedRequest);
1520
+ response = await fetch(signedRequest, {
1521
+ redirect: "manual",
1522
+ signal
1523
+ });
1524
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1525
+ const location = response.headers.get("Location");
1526
+ return doubleKnock(createRedirectRequest(request, location, body), identity, {
1527
+ ...options,
1528
+ body
1529
+ });
1530
+ }
1531
+ }
1532
+ if (fulfilled && response.status < 300) {
1533
+ await specDeterminer?.rememberSpec(origin, "rfc9421");
1534
+ return response;
1535
+ }
1536
+ if (fulfilled && response.status !== 400 && response.status !== 401) return response;
1537
+ }
1538
+ const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1539
+ logger.debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1313
1540
  spec: firstTrySpec,
1314
1541
  secondSpec: spec,
1315
1542
  status: response.status,
@@ -1364,4 +1591,4 @@ function timingSafeEqual(a, b) {
1364
1591
  }
1365
1592
 
1366
1593
  //#endregion
1367
- export { deno_default, doubleKnock, exportJwk, fetchKey, fetchKeyDetailed, generateCryptoKeyPair, importJwk, signRequest, validateCryptoKey, verifyRequest, verifyRequestDetailed };
1594
+ export { deno_default, doubleKnock, exportJwk, fetchKey, fetchKeyDetailed, formatAcceptSignature, fulfillAcceptSignature, generateCryptoKeyPair, importJwk, parseAcceptSignature, parseRfc9421SignatureInput, signRequest, validateAcceptSignature, validateCryptoKey, verifyRequest, verifyRequestDetailed };
@@ -3,7 +3,7 @@
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
  globalThis.addEventListener = () => {};
5
5
 
6
- import { deno_default } from "./deno-CEdy89j9.js";
6
+ import { deno_default } from "./deno-OR506Yti.js";
7
7
  import { Activity, getTypeId } from "@fedify/vocab";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -3,7 +3,7 @@
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
  globalThis.addEventListener = () => {};
5
5
 
6
- import { deno_default } from "./deno-CEdy89j9.js";
6
+ import { deno_default } from "./deno-OR506Yti.js";
7
7
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
8
8
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
9
9
  import { getLogger } from "@logtape/logtape";
@@ -3,7 +3,7 @@
3
3
  const { URLPattern } = require("urlpattern-polyfill");
4
4
 
5
5
  const require_chunk = require('./chunk-CGaQZ11T.cjs');
6
- const require_http = require('./http-iDlaLy8a.cjs');
6
+ const require_http = require('./http-CaXARmaJ.cjs');
7
7
  const __logtape_logtape = require_chunk.__toESM(require("@logtape/logtape"));
8
8
  const es_toolkit = require_chunk.__toESM(require("es-toolkit"));
9
9
  const __fedify_vocab_runtime = require_chunk.__toESM(require("@fedify/vocab-runtime"));
@@ -2,7 +2,7 @@
2
2
  import { Temporal } from "@js-temporal/polyfill";
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
 
5
- import { doubleKnock, validateCryptoKey } from "./http-VpqmUjje.js";
5
+ import { doubleKnock, validateCryptoKey } from "./http-DePHjWKP.js";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { curry } from "es-toolkit";
8
8
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
@@ -3,15 +3,15 @@
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
  globalThis.addEventListener = () => {};
5
5
 
6
- import { deno_default } from "./deno-CEdy89j9.js";
7
- import { fetchKey, validateCryptoKey } from "./key-B0yADkL8.js";
6
+ import { deno_default } from "./deno-OR506Yti.js";
7
+ import { fetchKey, validateCryptoKey } from "./key-Cx3Tx_In.js";
8
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
9
9
  import { getDocumentLoader } from "@fedify/vocab-runtime";
10
10
  import { getLogger } from "@logtape/logtape";
11
11
  import { SpanStatusCode, trace } from "@opentelemetry/api";
12
12
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
13
13
  import { encodeHex } from "byte-encodings/hex";
14
- import jsonld from "jsonld";
14
+ import jsonld from "@fedify/vocab-runtime/jsonld";
15
15
 
16
16
  //#region src/sig/ld.ts
17
17
  const logger = getLogger([