@fedify/fedify 2.1.0-dev.543 → 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 (100) 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-BBucr-Bp.js → builder-B24i8eYp.js} +4 -4
  4. package/dist/{client-Dg7OfUDA.js → client-CoCIaTNO.js} +1 -1
  5. package/dist/compat/mod.d.cts +3 -3
  6. package/dist/compat/mod.d.ts +3 -3
  7. package/dist/compat/transformers.test.js +19 -18
  8. package/dist/{context-CZ5llAss.js → context-Aqenou7c.js} +1 -1
  9. package/dist/{context-DL0cPpPV.d.cts → context-BcqA-0BL.d.cts} +52 -2
  10. package/dist/{context--RwChtri.d.ts → context-DyJjQQ_H.d.ts} +52 -2
  11. package/dist/{deno-9yc0TPBI.js → deno-OR506Yti.js} +1 -2
  12. package/dist/{docloader-6Wrqp6SE.js → docloader-BG_pP2fW.js} +3 -3
  13. package/dist/{esm-DGl7uK1r.js → esm-nLm00z9V.js} +27 -1
  14. package/dist/federation/builder.test.js +8 -8
  15. package/dist/federation/collection.test.js +6 -6
  16. package/dist/federation/handler.test.js +808 -28
  17. package/dist/federation/idempotency.test.js +24 -23
  18. package/dist/federation/inbox.test.js +4 -4
  19. package/dist/federation/keycache.test.js +2 -2
  20. package/dist/federation/kv.test.js +5 -5
  21. package/dist/federation/middleware.test.js +25 -24
  22. package/dist/federation/mod.cjs +4 -4
  23. package/dist/federation/mod.d.cts +4 -4
  24. package/dist/federation/mod.d.ts +4 -4
  25. package/dist/federation/mod.js +4 -4
  26. package/dist/federation/mq.test.js +5 -5
  27. package/dist/federation/negotiation.test.js +6 -6
  28. package/dist/federation/retry.test.js +3 -3
  29. package/dist/federation/router.test.js +5 -5
  30. package/dist/federation/send.test.js +13 -12
  31. package/dist/federation/webfinger.test.js +24 -23
  32. package/dist/{http-CpvoK0Y7.js → http-BUCxbGks.js} +145 -50
  33. package/dist/{http-DsqqmkXi.d.cts → http-BudnHZE2.d.cts} +229 -1
  34. package/dist/{http-C_9L2wFv.cjs → http-CaXARmaJ.cjs} +307 -50
  35. package/dist/{http-BbfOqHGG.d.ts → http-Dax_FIBo.d.ts} +229 -1
  36. package/dist/{http-DxwzIU0F.js → http-DePHjWKP.js} +278 -51
  37. package/dist/{inbox-DMq3a5bc.js → inbox-D_LU1opv.js} +2 -2
  38. package/dist/{key-DFG6tJgw.js → key-Cx3Tx_In.js} +2 -2
  39. package/dist/{kv-cache-B__dHl7g.js → kv-cache-Bw2F2ABq.js} +1 -1
  40. package/dist/{kv-cache-BHoLc85Z.cjs → kv-cache-CYTDBChd.cjs} +1 -1
  41. package/dist/{kv-cache-CRRUsyJ9.js → kv-cache-DizRqYX4.js} +1 -1
  42. package/dist/{ld-DVnRS9IK.js → ld-CLMJw_iX.js} +4 -4
  43. package/dist/{middleware-D6peKsn1.js → middleware--uATyG9i.js} +95 -18
  44. package/dist/{middleware-CAk-LkSS.js → middleware-4fo4pEtA.js} +4 -4
  45. package/dist/{middleware-pUJBhWSu.cjs → middleware-9YDezkYJ.cjs} +94 -17
  46. package/dist/middleware-C2PqSUaA.js +27 -0
  47. package/dist/middleware-DNY45l5T.cjs +12 -0
  48. package/dist/{middleware-FZ0T8vIp.js → middleware-DzICTgdC.js} +115 -36
  49. package/dist/{mod-DE8MYisy.d.cts → mod-B7QkWzrL.d.cts} +1 -1
  50. package/dist/{mod-DKG0ovjR.d.cts → mod-Bx9jcLB8.d.cts} +1 -1
  51. package/dist/{mod-CFBU2OT3.d.cts → mod-Coe7KEgX.d.cts} +1 -1
  52. package/dist/{mod-BugwI0JN.d.ts → mod-Cs2dYEwI.d.ts} +1 -1
  53. package/dist/{mod-DcfFNgYf.d.ts → mod-D6MdymW7.d.ts} +1 -1
  54. package/dist/{mod-CvxylbuV.d.ts → mod-D6dOd--H.d.ts} +1 -1
  55. package/dist/{mod-Z7lIaCfo.d.ts → mod-SMHOMNpZ.d.ts} +1 -1
  56. package/dist/{mod-Dp0kK0hO.d.cts → mod-em2Il1eD.d.cts} +1 -1
  57. package/dist/mod.cjs +12 -4
  58. package/dist/mod.d.cts +8 -8
  59. package/dist/mod.d.ts +8 -8
  60. package/dist/mod.js +9 -5
  61. package/dist/nodeinfo/client.test.js +7 -7
  62. package/dist/nodeinfo/handler.test.js +24 -23
  63. package/dist/nodeinfo/types.test.js +5 -5
  64. package/dist/otel/exporter.test.js +6 -6
  65. package/dist/{owner-MCqkZ1KE.js → owner-D5J299vd.js} +1 -1
  66. package/dist/{proof-D2B3jvnF.js → proof-BBLHhWMC.js} +3 -3
  67. package/dist/{proof-BF_LZjDb.cjs → proof-BVl5IgbN.cjs} +3 -3
  68. package/dist/{proof-ooYMfVCa.js → proof-CiCp_mCG.js} +2 -2
  69. package/dist/{send-DtP5YkuY.js → send-2b0Fn9cn.js} +2 -2
  70. package/dist/sig/accept.test.d.ts +3 -0
  71. package/dist/sig/accept.test.js +451 -0
  72. package/dist/sig/http.test.js +454 -29
  73. package/dist/sig/key.test.js +8 -8
  74. package/dist/sig/ld.test.js +7 -7
  75. package/dist/sig/mod.cjs +6 -2
  76. package/dist/sig/mod.d.cts +3 -3
  77. package/dist/sig/mod.d.ts +3 -3
  78. package/dist/sig/mod.js +3 -3
  79. package/dist/sig/owner.test.js +9 -9
  80. package/dist/sig/proof.test.js +9 -9
  81. package/dist/testing/mod.d.ts +1 -1
  82. package/dist/testing/mod.js +2 -2
  83. package/dist/utils/docloader.test.js +12 -11
  84. package/dist/utils/kv-cache.test.js +2 -2
  85. package/dist/utils/mod.cjs +2 -2
  86. package/dist/utils/mod.d.cts +2 -2
  87. package/dist/utils/mod.d.ts +2 -2
  88. package/dist/utils/mod.js +2 -2
  89. package/package.json +6 -7
  90. package/dist/dist-B5f6a8Tt.js +0 -281
  91. package/dist/middleware-D7yrgd0I.cjs +0 -12
  92. package/dist/middleware-GmHZnwkU.js +0 -26
  93. /package/dist/{assert_not_equals-C80BG-_5.js → assert_not_equals-f3m3epl3.js} +0 -0
  94. /package/dist/{assert_throws-BNXdRGWP.js → assert_throws-rjdMBf31.js} +0 -0
  95. /package/dist/{collection-CcnIw1qY.js → collection-CSzG2j1P.js} +0 -0
  96. /package/dist/{keycache-C7k8s1Bk.js → keycache-CpGWAUbj.js} +0 -0
  97. /package/dist/{keys-ZbcByPg9.js → keys-BFve7QQv.js} +0 -0
  98. /package/dist/{negotiation-5NPJL6zp.js → negotiation-BlAuS_nr.js} +0 -0
  99. /package/dist/{retry-D4GJ670a.js → retry-mqLf4b-R.js} +0 -0
  100. /package/dist/{std__assert-DWivtrGR.js → std__assert-X-_kMxKM.js} +0 -0
@@ -3,36 +3,37 @@
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
  globalThis.addEventListener = () => {};
5
5
 
6
- import { createTestTracerProvider, mockDocumentLoader, test } from "../dist-B5f6a8Tt.js";
7
6
  import { assertEquals } from "../assert_equals-DSbWqCm3.js";
8
7
  import { assert } from "../assert-MZs1qjMx.js";
9
8
  import "../assert_instance_of-DHz7EHNU.js";
10
9
  import { MemoryKvStore } from "../kv-QzKcOQgP.js";
11
- import "../deno-9yc0TPBI.js";
12
- import { createFederation, handleActor, handleCollection, handleCustomCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable } from "../middleware-FZ0T8vIp.js";
13
- import "../client-Dg7OfUDA.js";
10
+ import "../deno-OR506Yti.js";
11
+ import { createFederation, handleActor, handleCollection, handleCustomCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable } from "../middleware-DzICTgdC.js";
12
+ import "../client-CoCIaTNO.js";
14
13
  import "../router-D9eI0s4b.js";
15
14
  import "../types-CPz01LGH.js";
16
- import "../key-DFG6tJgw.js";
17
- import { signRequest } from "../http-CpvoK0Y7.js";
18
- import "../ld-DVnRS9IK.js";
19
- import "../owner-MCqkZ1KE.js";
20
- import "../proof-D2B3jvnF.js";
21
- import "../docloader-6Wrqp6SE.js";
22
- import "../kv-cache-B__dHl7g.js";
23
- import { InboxListenerSet } from "../inbox-DMq3a5bc.js";
24
- import "../builder-BBucr-Bp.js";
25
- import "../collection-CcnIw1qY.js";
26
- import "../keycache-C7k8s1Bk.js";
27
- import "../negotiation-5NPJL6zp.js";
28
- import "../retry-D4GJ670a.js";
29
- import "../send-DtP5YkuY.js";
30
- import "../std__assert-DWivtrGR.js";
31
- import "../assert_rejects-Ce45JcFg.js";
32
- import "../assert_throws-BNXdRGWP.js";
33
- import "../assert_not_equals-C80BG-_5.js";
34
- import { createInboxContext, createRequestContext } from "../context-CZ5llAss.js";
35
- import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3 } from "../keys-ZbcByPg9.js";
15
+ import { parseAcceptSignature } from "../accept-D7sAxyNa.js";
16
+ import "../key-Cx3Tx_In.js";
17
+ import { signRequest } from "../http-BUCxbGks.js";
18
+ import "../ld-CLMJw_iX.js";
19
+ import "../owner-D5J299vd.js";
20
+ import "../proof-BBLHhWMC.js";
21
+ import "../docloader-BG_pP2fW.js";
22
+ import "../kv-cache-Bw2F2ABq.js";
23
+ import { InboxListenerSet } from "../inbox-D_LU1opv.js";
24
+ import "../builder-B24i8eYp.js";
25
+ import "../collection-CSzG2j1P.js";
26
+ import "../keycache-CpGWAUbj.js";
27
+ import "../negotiation-BlAuS_nr.js";
28
+ import "../retry-mqLf4b-R.js";
29
+ import "../send-2b0Fn9cn.js";
30
+ import "../std__assert-X-_kMxKM.js";
31
+ import "../assert_rejects-0h7I2Esa.js";
32
+ import "../assert_throws-rjdMBf31.js";
33
+ import "../assert_not_equals-f3m3epl3.js";
34
+ import { createInboxContext, createRequestContext } from "../context-Aqenou7c.js";
35
+ import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3 } from "../keys-BFve7QQv.js";
36
+ import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
36
37
  import { Create, Note, Person } from "@fedify/vocab";
37
38
  import { FetchError } from "@fedify/vocab-runtime";
38
39
 
@@ -914,7 +915,8 @@ test("handleInbox()", async () => {
914
915
  kv: new MemoryKvStore(),
915
916
  kvPrefixes: {
916
917
  activityIdempotence: ["_fedify", "activityIdempotence"],
917
- publicKey: ["_fedify", "publicKey"]
918
+ publicKey: ["_fedify", "publicKey"],
919
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
918
920
  },
919
921
  actorDispatcher,
920
922
  onNotFound,
@@ -1160,7 +1162,8 @@ test("handleInbox() - authentication bypass vulnerability", async () => {
1160
1162
  kv: new MemoryKvStore(),
1161
1163
  kvPrefixes: {
1162
1164
  activityIdempotence: ["_fedify", "activityIdempotence"],
1163
- publicKey: ["_fedify", "publicKey"]
1165
+ publicKey: ["_fedify", "publicKey"],
1166
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1164
1167
  },
1165
1168
  actorDispatcher,
1166
1169
  inboxListeners,
@@ -1556,7 +1559,8 @@ test("handleInbox() records OpenTelemetry span events", async () => {
1556
1559
  kv,
1557
1560
  kvPrefixes: {
1558
1561
  activityIdempotence: ["activityIdempotence"],
1559
- publicKey: ["publicKey"]
1562
+ publicKey: ["publicKey"],
1563
+ acceptSignatureNonce: ["acceptSignatureNonce"]
1560
1564
  },
1561
1565
  actorDispatcher,
1562
1566
  inboxListeners: listeners,
@@ -1642,7 +1646,8 @@ test("handleInbox() records unverified HTTP signature details", async () => {
1642
1646
  kv,
1643
1647
  kvPrefixes: {
1644
1648
  activityIdempotence: ["activityIdempotence"],
1645
- publicKey: ["publicKey"]
1649
+ publicKey: ["publicKey"],
1650
+ acceptSignatureNonce: ["acceptSignatureNonce"]
1646
1651
  },
1647
1652
  actorDispatcher,
1648
1653
  inboxListeners: new InboxListenerSet(),
@@ -1667,5 +1672,780 @@ test("handleInbox() records unverified HTTP signature details", async () => {
1667
1672
  assertEquals(event.attributes["http_signatures.failure_reason"], "keyFetchError");
1668
1673
  assertEquals(event.attributes["http_signatures.key_fetch_status"], 410);
1669
1674
  });
1675
+ test("handleInbox() challenge policy enabled + unsigned request", async () => {
1676
+ const activity = new Create({
1677
+ id: new URL("https://example.com/activities/challenge-1"),
1678
+ actor: new URL("https://example.com/person2"),
1679
+ object: new Note({
1680
+ id: new URL("https://example.com/notes/challenge-1"),
1681
+ attribution: new URL("https://example.com/person2"),
1682
+ content: "Hello!"
1683
+ })
1684
+ });
1685
+ const unsignedRequest = new Request("https://example.com/", {
1686
+ method: "POST",
1687
+ body: JSON.stringify(await activity.toJsonLd())
1688
+ });
1689
+ const federation = createFederation({ kv: new MemoryKvStore() });
1690
+ const context = createRequestContext({
1691
+ federation,
1692
+ request: unsignedRequest,
1693
+ url: new URL(unsignedRequest.url),
1694
+ data: void 0
1695
+ });
1696
+ const actorDispatcher = (_ctx, identifier) => {
1697
+ if (identifier !== "someone") return null;
1698
+ return new Person({ name: "Someone" });
1699
+ };
1700
+ const kv = new MemoryKvStore();
1701
+ const response = await handleInbox(unsignedRequest, {
1702
+ recipient: "someone",
1703
+ context,
1704
+ inboxContextFactory(_activity) {
1705
+ return createInboxContext({
1706
+ ...context,
1707
+ clone: void 0,
1708
+ recipient: "someone"
1709
+ });
1710
+ },
1711
+ kv,
1712
+ kvPrefixes: {
1713
+ activityIdempotence: ["_fedify", "activityIdempotence"],
1714
+ publicKey: ["_fedify", "publicKey"],
1715
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1716
+ },
1717
+ actorDispatcher,
1718
+ onNotFound: () => new Response("Not found", { status: 404 }),
1719
+ signatureTimeWindow: { minutes: 5 },
1720
+ skipSignatureVerification: false,
1721
+ inboxChallengePolicy: { enabled: true }
1722
+ });
1723
+ assertEquals(response.status, 401);
1724
+ const acceptSig = response.headers.get("Accept-Signature");
1725
+ assert(acceptSig != null, "Accept-Signature header must be present");
1726
+ const parsed = parseAcceptSignature(acceptSig);
1727
+ assert(parsed.length > 0, "Accept-Signature must have at least one entry");
1728
+ assertEquals(parsed[0].label, "sig1");
1729
+ assert(parsed[0].components.some((c) => c.value === "@method"), "Must include @method component");
1730
+ assertEquals(response.headers.get("Cache-Control"), "no-store");
1731
+ assertEquals(response.headers.get("Vary"), "Accept, Signature");
1732
+ });
1733
+ test("handleInbox() challenge policy enabled + invalid signature", async () => {
1734
+ const activity = new Create({
1735
+ id: new URL("https://example.com/activities/challenge-2"),
1736
+ actor: new URL("https://example.com/person2"),
1737
+ object: new Note({
1738
+ id: new URL("https://example.com/notes/challenge-2"),
1739
+ attribution: new URL("https://example.com/person2"),
1740
+ content: "Hello!"
1741
+ })
1742
+ });
1743
+ const originalRequest = new Request("https://example.com/", {
1744
+ method: "POST",
1745
+ body: JSON.stringify(await activity.toJsonLd())
1746
+ });
1747
+ const signedRequest = await signRequest(originalRequest, rsaPrivateKey3, rsaPublicKey3.id);
1748
+ const jsonLd = await activity.toJsonLd();
1749
+ const tamperedBody = JSON.stringify({
1750
+ ...jsonLd,
1751
+ "https://example.com/tampered": true
1752
+ });
1753
+ const tamperedRequest = new Request(signedRequest.url, {
1754
+ method: signedRequest.method,
1755
+ headers: signedRequest.headers,
1756
+ body: tamperedBody
1757
+ });
1758
+ const federation = createFederation({ kv: new MemoryKvStore() });
1759
+ const context = createRequestContext({
1760
+ federation,
1761
+ request: tamperedRequest,
1762
+ url: new URL(tamperedRequest.url),
1763
+ data: void 0,
1764
+ documentLoader: mockDocumentLoader
1765
+ });
1766
+ const actorDispatcher = (_ctx, identifier) => {
1767
+ if (identifier !== "someone") return null;
1768
+ return new Person({ name: "Someone" });
1769
+ };
1770
+ const kv = new MemoryKvStore();
1771
+ const response = await handleInbox(tamperedRequest, {
1772
+ recipient: "someone",
1773
+ context,
1774
+ inboxContextFactory(_activity) {
1775
+ return createInboxContext({
1776
+ ...context,
1777
+ clone: void 0,
1778
+ recipient: "someone"
1779
+ });
1780
+ },
1781
+ kv,
1782
+ kvPrefixes: {
1783
+ activityIdempotence: ["_fedify", "activityIdempotence"],
1784
+ publicKey: ["_fedify", "publicKey"],
1785
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1786
+ },
1787
+ actorDispatcher,
1788
+ onNotFound: () => new Response("Not found", { status: 404 }),
1789
+ signatureTimeWindow: { minutes: 5 },
1790
+ skipSignatureVerification: false,
1791
+ inboxChallengePolicy: { enabled: true }
1792
+ });
1793
+ assertEquals(response.status, 401);
1794
+ const acceptSig = response.headers.get("Accept-Signature");
1795
+ assert(acceptSig != null, "Accept-Signature header must be present");
1796
+ assertEquals(response.headers.get("Cache-Control"), "no-store");
1797
+ });
1798
+ test("handleInbox() challenge policy enabled + valid signature", async () => {
1799
+ const activity = new Create({
1800
+ id: new URL("https://example.com/activities/challenge-3"),
1801
+ actor: new URL("https://example.com/person2"),
1802
+ object: new Note({
1803
+ id: new URL("https://example.com/notes/challenge-3"),
1804
+ attribution: new URL("https://example.com/person2"),
1805
+ content: "Hello!"
1806
+ })
1807
+ });
1808
+ const federation = createFederation({ kv: new MemoryKvStore() });
1809
+ const signedRequest = await signRequest(new Request("https://example.com/", {
1810
+ method: "POST",
1811
+ body: JSON.stringify(await activity.toJsonLd())
1812
+ }), rsaPrivateKey3, rsaPublicKey3.id);
1813
+ const context = createRequestContext({
1814
+ federation,
1815
+ request: signedRequest,
1816
+ url: new URL(signedRequest.url),
1817
+ data: void 0,
1818
+ documentLoader: mockDocumentLoader
1819
+ });
1820
+ const actorDispatcher = (_ctx, identifier) => {
1821
+ if (identifier !== "someone") return null;
1822
+ return new Person({ name: "Someone" });
1823
+ };
1824
+ const kv = new MemoryKvStore();
1825
+ const response = await handleInbox(signedRequest, {
1826
+ recipient: "someone",
1827
+ context,
1828
+ inboxContextFactory(_activity) {
1829
+ return createInboxContext({
1830
+ ...context,
1831
+ clone: void 0,
1832
+ recipient: "someone"
1833
+ });
1834
+ },
1835
+ kv,
1836
+ kvPrefixes: {
1837
+ activityIdempotence: ["_fedify", "activityIdempotence"],
1838
+ publicKey: ["_fedify", "publicKey"],
1839
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1840
+ },
1841
+ actorDispatcher,
1842
+ onNotFound: () => new Response("Not found", { status: 404 }),
1843
+ signatureTimeWindow: { minutes: 5 },
1844
+ skipSignatureVerification: false,
1845
+ inboxChallengePolicy: { enabled: true }
1846
+ });
1847
+ assertEquals(response.status, 202);
1848
+ assertEquals(response.headers.get("Accept-Signature"), null, "No Accept-Signature header on successful request");
1849
+ });
1850
+ test("handleInbox() challenge policy disabled + unsigned request", async () => {
1851
+ const activity = new Create({
1852
+ id: new URL("https://example.com/activities/challenge-4"),
1853
+ actor: new URL("https://example.com/person2"),
1854
+ object: new Note({
1855
+ id: new URL("https://example.com/notes/challenge-4"),
1856
+ attribution: new URL("https://example.com/person2"),
1857
+ content: "Hello!"
1858
+ })
1859
+ });
1860
+ const unsignedRequest = new Request("https://example.com/", {
1861
+ method: "POST",
1862
+ body: JSON.stringify(await activity.toJsonLd())
1863
+ });
1864
+ const federation = createFederation({ kv: new MemoryKvStore() });
1865
+ const context = createRequestContext({
1866
+ federation,
1867
+ request: unsignedRequest,
1868
+ url: new URL(unsignedRequest.url),
1869
+ data: void 0
1870
+ });
1871
+ const actorDispatcher = (_ctx, identifier) => {
1872
+ if (identifier !== "someone") return null;
1873
+ return new Person({ name: "Someone" });
1874
+ };
1875
+ const kv = new MemoryKvStore();
1876
+ const response = await handleInbox(unsignedRequest, {
1877
+ recipient: "someone",
1878
+ context,
1879
+ inboxContextFactory(_activity) {
1880
+ return createInboxContext({
1881
+ ...context,
1882
+ clone: void 0,
1883
+ recipient: "someone"
1884
+ });
1885
+ },
1886
+ kv,
1887
+ kvPrefixes: {
1888
+ activityIdempotence: ["_fedify", "activityIdempotence"],
1889
+ publicKey: ["_fedify", "publicKey"],
1890
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1891
+ },
1892
+ actorDispatcher,
1893
+ onNotFound: () => new Response("Not found", { status: 404 }),
1894
+ signatureTimeWindow: { minutes: 5 },
1895
+ skipSignatureVerification: false
1896
+ });
1897
+ assertEquals(response.status, 401);
1898
+ assertEquals(response.headers.get("Accept-Signature"), null, "No Accept-Signature header when challenge policy is disabled");
1899
+ });
1900
+ test("handleInbox() actor/key mismatch → plain 401 (no challenge)", async () => {
1901
+ const maliciousActivity = new Create({
1902
+ id: new URL("https://attacker.example.com/activities/challenge-5"),
1903
+ actor: new URL("https://victim.example.com/users/alice"),
1904
+ object: new Note({
1905
+ id: new URL("https://attacker.example.com/notes/challenge-5"),
1906
+ attribution: new URL("https://victim.example.com/users/alice"),
1907
+ content: "Forged message!"
1908
+ })
1909
+ });
1910
+ const maliciousRequest = await signRequest(new Request("https://example.com/", {
1911
+ method: "POST",
1912
+ body: JSON.stringify(await maliciousActivity.toJsonLd())
1913
+ }), rsaPrivateKey3, rsaPublicKey3.id);
1914
+ const federation = createFederation({ kv: new MemoryKvStore() });
1915
+ const context = createRequestContext({
1916
+ federation,
1917
+ request: maliciousRequest,
1918
+ url: new URL(maliciousRequest.url),
1919
+ data: void 0,
1920
+ documentLoader: mockDocumentLoader
1921
+ });
1922
+ const actorDispatcher = (_ctx, identifier) => {
1923
+ if (identifier !== "someone") return null;
1924
+ return new Person({ name: "Someone" });
1925
+ };
1926
+ const kv = new MemoryKvStore();
1927
+ const response = await handleInbox(maliciousRequest, {
1928
+ recipient: "someone",
1929
+ context,
1930
+ inboxContextFactory(_activity) {
1931
+ return createInboxContext({
1932
+ ...context,
1933
+ clone: void 0,
1934
+ recipient: "someone"
1935
+ });
1936
+ },
1937
+ kv,
1938
+ kvPrefixes: {
1939
+ activityIdempotence: ["_fedify", "activityIdempotence"],
1940
+ publicKey: ["_fedify", "publicKey"],
1941
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1942
+ },
1943
+ actorDispatcher,
1944
+ onNotFound: () => new Response("Not found", { status: 404 }),
1945
+ signatureTimeWindow: { minutes: 5 },
1946
+ skipSignatureVerification: false,
1947
+ inboxChallengePolicy: { enabled: true }
1948
+ });
1949
+ assertEquals(response.status, 401);
1950
+ assertEquals(response.headers.get("Accept-Signature"), null, "Actor/key mismatch should not emit Accept-Signature challenge");
1951
+ assertEquals(await response.text(), "The signer and the actor do not match.");
1952
+ });
1953
+ test("handleInbox() nonce issuance in challenge", async () => {
1954
+ const activity = new Create({
1955
+ id: new URL("https://example.com/activities/nonce-1"),
1956
+ actor: new URL("https://example.com/person2"),
1957
+ object: new Note({
1958
+ id: new URL("https://example.com/notes/nonce-1"),
1959
+ attribution: new URL("https://example.com/person2"),
1960
+ content: "Hello!"
1961
+ })
1962
+ });
1963
+ const unsignedRequest = new Request("https://example.com/", {
1964
+ method: "POST",
1965
+ body: JSON.stringify(await activity.toJsonLd())
1966
+ });
1967
+ const federation = createFederation({ kv: new MemoryKvStore() });
1968
+ const context = createRequestContext({
1969
+ federation,
1970
+ request: unsignedRequest,
1971
+ url: new URL(unsignedRequest.url),
1972
+ data: void 0
1973
+ });
1974
+ const actorDispatcher = (_ctx, identifier) => {
1975
+ if (identifier !== "someone") return null;
1976
+ return new Person({ name: "Someone" });
1977
+ };
1978
+ const kv = new MemoryKvStore();
1979
+ const response = await handleInbox(unsignedRequest, {
1980
+ recipient: "someone",
1981
+ context,
1982
+ inboxContextFactory(_activity) {
1983
+ return createInboxContext({
1984
+ ...context,
1985
+ clone: void 0,
1986
+ recipient: "someone"
1987
+ });
1988
+ },
1989
+ kv,
1990
+ kvPrefixes: {
1991
+ activityIdempotence: ["_fedify", "activityIdempotence"],
1992
+ publicKey: ["_fedify", "publicKey"],
1993
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
1994
+ },
1995
+ actorDispatcher,
1996
+ onNotFound: () => new Response("Not found", { status: 404 }),
1997
+ signatureTimeWindow: { minutes: 5 },
1998
+ skipSignatureVerification: false,
1999
+ inboxChallengePolicy: {
2000
+ enabled: true,
2001
+ requestNonce: true,
2002
+ nonceTtlSeconds: 300
2003
+ }
2004
+ });
2005
+ assertEquals(response.status, 401);
2006
+ const acceptSig = response.headers.get("Accept-Signature");
2007
+ assert(acceptSig != null, "Accept-Signature header must be present");
2008
+ const parsed = parseAcceptSignature(acceptSig);
2009
+ assert(parsed.length > 0);
2010
+ assert(parsed[0].parameters.nonce != null, "Nonce must be present in Accept-Signature parameters");
2011
+ assertEquals(response.headers.get("Cache-Control"), "no-store");
2012
+ const nonceKey = [
2013
+ "_fedify",
2014
+ "acceptSignatureNonce",
2015
+ parsed[0].parameters.nonce
2016
+ ];
2017
+ const stored = await kv.get(nonceKey);
2018
+ assertEquals(stored, true, "Nonce must be stored in KV store");
2019
+ });
2020
+ test("handleInbox() nonce consumption on valid signed request", async () => {
2021
+ const activity = new Create({
2022
+ id: new URL("https://example.com/activities/nonce-2"),
2023
+ actor: new URL("https://example.com/person2"),
2024
+ object: new Note({
2025
+ id: new URL("https://example.com/notes/nonce-2"),
2026
+ attribution: new URL("https://example.com/person2"),
2027
+ content: "Hello!"
2028
+ })
2029
+ });
2030
+ const kv = new MemoryKvStore();
2031
+ const noncePrefix = ["_fedify", "acceptSignatureNonce"];
2032
+ const nonce = "test-nonce-abc123";
2033
+ await kv.set([
2034
+ "_fedify",
2035
+ "acceptSignatureNonce",
2036
+ nonce
2037
+ ], true, { ttl: Temporal.Duration.from({ seconds: 300 }) });
2038
+ const signedRequest = await signRequest(new Request("https://example.com/", {
2039
+ method: "POST",
2040
+ body: JSON.stringify(await activity.toJsonLd())
2041
+ }), rsaPrivateKey3, rsaPublicKey3.id, {
2042
+ spec: "rfc9421",
2043
+ rfc9421: { nonce }
2044
+ });
2045
+ const federation = createFederation({ kv: new MemoryKvStore() });
2046
+ const context = createRequestContext({
2047
+ federation,
2048
+ request: signedRequest,
2049
+ url: new URL(signedRequest.url),
2050
+ data: void 0,
2051
+ documentLoader: mockDocumentLoader
2052
+ });
2053
+ const actorDispatcher = (_ctx, identifier) => {
2054
+ if (identifier !== "someone") return null;
2055
+ return new Person({ name: "Someone" });
2056
+ };
2057
+ const response = await handleInbox(signedRequest, {
2058
+ recipient: "someone",
2059
+ context,
2060
+ inboxContextFactory(_activity) {
2061
+ return createInboxContext({
2062
+ ...context,
2063
+ clone: void 0,
2064
+ recipient: "someone"
2065
+ });
2066
+ },
2067
+ kv,
2068
+ kvPrefixes: {
2069
+ activityIdempotence: ["_fedify", "activityIdempotence"],
2070
+ publicKey: ["_fedify", "publicKey"],
2071
+ acceptSignatureNonce: noncePrefix
2072
+ },
2073
+ actorDispatcher,
2074
+ onNotFound: () => new Response("Not found", { status: 404 }),
2075
+ signatureTimeWindow: { minutes: 5 },
2076
+ skipSignatureVerification: false,
2077
+ inboxChallengePolicy: {
2078
+ enabled: true,
2079
+ requestNonce: true,
2080
+ nonceTtlSeconds: 300
2081
+ }
2082
+ });
2083
+ assertEquals(response.status, 202);
2084
+ const stored = await kv.get([
2085
+ "_fedify",
2086
+ "acceptSignatureNonce",
2087
+ nonce
2088
+ ]);
2089
+ assertEquals(stored, void 0, "Nonce must be consumed after use");
2090
+ });
2091
+ test("handleInbox() nonce replay prevention", async () => {
2092
+ const activity = new Create({
2093
+ id: new URL("https://example.com/activities/nonce-3"),
2094
+ actor: new URL("https://example.com/person2"),
2095
+ object: new Note({
2096
+ id: new URL("https://example.com/notes/nonce-3"),
2097
+ attribution: new URL("https://example.com/person2"),
2098
+ content: "Hello!"
2099
+ })
2100
+ });
2101
+ const kv = new MemoryKvStore();
2102
+ const noncePrefix = ["_fedify", "acceptSignatureNonce"];
2103
+ const nonce = "replay-nonce-xyz";
2104
+ const signedRequest = await signRequest(new Request("https://example.com/", {
2105
+ method: "POST",
2106
+ body: JSON.stringify(await activity.toJsonLd())
2107
+ }), rsaPrivateKey3, rsaPublicKey3.id, {
2108
+ spec: "rfc9421",
2109
+ rfc9421: { nonce }
2110
+ });
2111
+ const federation = createFederation({ kv: new MemoryKvStore() });
2112
+ const context = createRequestContext({
2113
+ federation,
2114
+ request: signedRequest,
2115
+ url: new URL(signedRequest.url),
2116
+ data: void 0,
2117
+ documentLoader: mockDocumentLoader
2118
+ });
2119
+ const actorDispatcher = (_ctx, identifier) => {
2120
+ if (identifier !== "someone") return null;
2121
+ return new Person({ name: "Someone" });
2122
+ };
2123
+ const response = await handleInbox(signedRequest, {
2124
+ recipient: "someone",
2125
+ context,
2126
+ inboxContextFactory(_activity) {
2127
+ return createInboxContext({
2128
+ ...context,
2129
+ clone: void 0,
2130
+ recipient: "someone"
2131
+ });
2132
+ },
2133
+ kv,
2134
+ kvPrefixes: {
2135
+ activityIdempotence: ["_fedify", "activityIdempotence"],
2136
+ publicKey: ["_fedify", "publicKey"],
2137
+ acceptSignatureNonce: noncePrefix
2138
+ },
2139
+ actorDispatcher,
2140
+ onNotFound: () => new Response("Not found", { status: 404 }),
2141
+ signatureTimeWindow: { minutes: 5 },
2142
+ skipSignatureVerification: false,
2143
+ inboxChallengePolicy: {
2144
+ enabled: true,
2145
+ requestNonce: true,
2146
+ nonceTtlSeconds: 300
2147
+ }
2148
+ });
2149
+ assertEquals(response.status, 401);
2150
+ const acceptSig = response.headers.get("Accept-Signature");
2151
+ assert(acceptSig != null, "Must emit fresh Accept-Signature challenge");
2152
+ const parsed = parseAcceptSignature(acceptSig);
2153
+ assert(parsed.length > 0);
2154
+ assert(parsed[0].parameters.nonce != null, "Fresh challenge must include a new nonce");
2155
+ assert(parsed[0].parameters.nonce !== nonce, "Fresh nonce must differ from the replayed one");
2156
+ assertEquals(response.headers.get("Cache-Control"), "no-store", "Challenge response must have Cache-Control: no-store");
2157
+ });
2158
+ test("handleInbox() nonce bypass: valid sig without nonce + invalid sig with nonce", async () => {
2159
+ const activity = new Create({
2160
+ id: new URL("https://example.com/activities/nonce-bypass-1"),
2161
+ actor: new URL("https://example.com/person2"),
2162
+ object: new Note({
2163
+ id: new URL("https://example.com/notes/nonce-bypass-1"),
2164
+ attribution: new URL("https://example.com/person2"),
2165
+ content: "Hello!"
2166
+ })
2167
+ });
2168
+ const kv = new MemoryKvStore();
2169
+ const noncePrefix = ["_fedify", "acceptSignatureNonce"];
2170
+ const storedNonce = "bypass-nonce-abc123";
2171
+ await kv.set([
2172
+ "_fedify",
2173
+ "acceptSignatureNonce",
2174
+ storedNonce
2175
+ ], true, { ttl: Temporal.Duration.from({ seconds: 300 }) });
2176
+ const signedRequest = await signRequest(new Request("https://example.com/", {
2177
+ method: "POST",
2178
+ body: JSON.stringify(await activity.toJsonLd())
2179
+ }), rsaPrivateKey3, rsaPublicKey3.id, { spec: "rfc9421" });
2180
+ const existingSignatureInput = signedRequest.headers.get("Signature-Input");
2181
+ const existingSignature = signedRequest.headers.get("Signature");
2182
+ const bogusSigInput = `sig2=("@method" "@target-uri");alg="rsa-v1_5-sha256";keyid="${rsaPublicKey3.id.href}";created=${Math.floor(Date.now() / 1e3)};nonce="${storedNonce}"`;
2183
+ const bogusSigValue = `sig2=:AAAA:`;
2184
+ const tamperedHeaders = new Headers(signedRequest.headers);
2185
+ tamperedHeaders.set("Signature-Input", `${existingSignatureInput}, ${bogusSigInput}`);
2186
+ tamperedHeaders.set("Signature", `${existingSignature}, ${bogusSigValue}`);
2187
+ const tamperedRequest = new Request(signedRequest.url, {
2188
+ method: signedRequest.method,
2189
+ headers: tamperedHeaders,
2190
+ body: await signedRequest.clone().arrayBuffer()
2191
+ });
2192
+ const federation = createFederation({ kv: new MemoryKvStore() });
2193
+ const context = createRequestContext({
2194
+ federation,
2195
+ request: tamperedRequest,
2196
+ url: new URL(tamperedRequest.url),
2197
+ data: void 0,
2198
+ documentLoader: mockDocumentLoader
2199
+ });
2200
+ const actorDispatcher = (_ctx, identifier) => {
2201
+ if (identifier !== "someone") return null;
2202
+ return new Person({ name: "Someone" });
2203
+ };
2204
+ const response = await handleInbox(tamperedRequest, {
2205
+ recipient: "someone",
2206
+ context,
2207
+ inboxContextFactory(_activity) {
2208
+ return createInboxContext({
2209
+ ...context,
2210
+ clone: void 0,
2211
+ recipient: "someone"
2212
+ });
2213
+ },
2214
+ kv,
2215
+ kvPrefixes: {
2216
+ activityIdempotence: ["_fedify", "activityIdempotence"],
2217
+ publicKey: ["_fedify", "publicKey"],
2218
+ acceptSignatureNonce: noncePrefix
2219
+ },
2220
+ actorDispatcher,
2221
+ onNotFound: () => new Response("Not found", { status: 404 }),
2222
+ signatureTimeWindow: { minutes: 5 },
2223
+ skipSignatureVerification: false,
2224
+ inboxChallengePolicy: {
2225
+ enabled: true,
2226
+ requestNonce: true,
2227
+ nonceTtlSeconds: 300
2228
+ }
2229
+ });
2230
+ assertEquals(response.status, 401, "Request with nonce only in a non-verified signature must be rejected (nonce verification must be bound to the verified signature label)");
2231
+ const stored = await kv.get([
2232
+ "_fedify",
2233
+ "acceptSignatureNonce",
2234
+ storedNonce
2235
+ ]);
2236
+ assertEquals(stored, true, "Nonce must not be consumed when it comes from a non-verified signature");
2237
+ });
2238
+ test("handleInbox() actor/key mismatch does not consume nonce", async () => {
2239
+ const maliciousActivity = new Create({
2240
+ id: new URL("https://attacker.example.com/activities/mismatch-nonce-1"),
2241
+ actor: new URL("https://victim.example.com/users/alice"),
2242
+ object: new Note({
2243
+ id: new URL("https://attacker.example.com/notes/mismatch-nonce-1"),
2244
+ attribution: new URL("https://victim.example.com/users/alice"),
2245
+ content: "Forged message with nonce!"
2246
+ })
2247
+ });
2248
+ const kv = new MemoryKvStore();
2249
+ const noncePrefix = ["_fedify", "acceptSignatureNonce"];
2250
+ const nonce = "mismatch-nonce-xyz";
2251
+ await kv.set([
2252
+ "_fedify",
2253
+ "acceptSignatureNonce",
2254
+ nonce
2255
+ ], true, { ttl: Temporal.Duration.from({ seconds: 300 }) });
2256
+ const maliciousRequest = await signRequest(new Request("https://example.com/", {
2257
+ method: "POST",
2258
+ body: JSON.stringify(await maliciousActivity.toJsonLd())
2259
+ }), rsaPrivateKey3, rsaPublicKey3.id, {
2260
+ spec: "rfc9421",
2261
+ rfc9421: { nonce }
2262
+ });
2263
+ const federation = createFederation({ kv: new MemoryKvStore() });
2264
+ const context = createRequestContext({
2265
+ federation,
2266
+ request: maliciousRequest,
2267
+ url: new URL(maliciousRequest.url),
2268
+ data: void 0,
2269
+ documentLoader: mockDocumentLoader
2270
+ });
2271
+ const actorDispatcher = (_ctx, identifier) => {
2272
+ if (identifier !== "someone") return null;
2273
+ return new Person({ name: "Someone" });
2274
+ };
2275
+ const response = await handleInbox(maliciousRequest, {
2276
+ recipient: "someone",
2277
+ context,
2278
+ inboxContextFactory(_activity) {
2279
+ return createInboxContext({
2280
+ ...context,
2281
+ clone: void 0,
2282
+ recipient: "someone"
2283
+ });
2284
+ },
2285
+ kv,
2286
+ kvPrefixes: {
2287
+ activityIdempotence: ["_fedify", "activityIdempotence"],
2288
+ publicKey: ["_fedify", "publicKey"],
2289
+ acceptSignatureNonce: noncePrefix
2290
+ },
2291
+ actorDispatcher,
2292
+ onNotFound: () => new Response("Not found", { status: 404 }),
2293
+ signatureTimeWindow: { minutes: 5 },
2294
+ skipSignatureVerification: false,
2295
+ inboxChallengePolicy: {
2296
+ enabled: true,
2297
+ requestNonce: true,
2298
+ nonceTtlSeconds: 300
2299
+ }
2300
+ });
2301
+ assertEquals(response.status, 401);
2302
+ assertEquals(await response.text(), "The signer and the actor do not match.");
2303
+ const stored = await kv.get([
2304
+ "_fedify",
2305
+ "acceptSignatureNonce",
2306
+ nonce
2307
+ ]);
2308
+ assertEquals(stored, true, "Nonce must not be consumed when actor/key ownership check fails");
2309
+ });
2310
+ test("handleInbox() challenge policy enabled + unverifiedActivityHandler returns undefined", async () => {
2311
+ const activity = new Create({
2312
+ id: new URL("https://example.com/activities/challenge-unverified"),
2313
+ actor: new URL("https://example.com/person2"),
2314
+ object: new Note({
2315
+ id: new URL("https://example.com/notes/challenge-unverified"),
2316
+ attribution: new URL("https://example.com/person2"),
2317
+ content: "Hello!"
2318
+ })
2319
+ });
2320
+ const originalRequest = new Request("https://example.com/", {
2321
+ method: "POST",
2322
+ body: JSON.stringify(await activity.toJsonLd())
2323
+ });
2324
+ const signedRequest = await signRequest(originalRequest, rsaPrivateKey3, rsaPublicKey3.id);
2325
+ const jsonLd = await activity.toJsonLd();
2326
+ const tamperedBody = JSON.stringify({
2327
+ ...jsonLd,
2328
+ "https://example.com/tampered": true
2329
+ });
2330
+ const tamperedRequest = new Request(signedRequest.url, {
2331
+ method: signedRequest.method,
2332
+ headers: signedRequest.headers,
2333
+ body: tamperedBody
2334
+ });
2335
+ const federation = createFederation({ kv: new MemoryKvStore() });
2336
+ const context = createRequestContext({
2337
+ federation,
2338
+ request: tamperedRequest,
2339
+ url: new URL(tamperedRequest.url),
2340
+ data: void 0,
2341
+ documentLoader: mockDocumentLoader
2342
+ });
2343
+ const actorDispatcher = (_ctx, identifier) => {
2344
+ if (identifier !== "someone") return null;
2345
+ return new Person({ name: "Someone" });
2346
+ };
2347
+ const kv = new MemoryKvStore();
2348
+ const response = await handleInbox(tamperedRequest, {
2349
+ recipient: "someone",
2350
+ context,
2351
+ inboxContextFactory(_activity) {
2352
+ return createInboxContext({
2353
+ ...context,
2354
+ clone: void 0,
2355
+ recipient: "someone"
2356
+ });
2357
+ },
2358
+ kv,
2359
+ kvPrefixes: {
2360
+ activityIdempotence: ["_fedify", "activityIdempotence"],
2361
+ publicKey: ["_fedify", "publicKey"],
2362
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
2363
+ },
2364
+ actorDispatcher,
2365
+ unverifiedActivityHandler() {},
2366
+ onNotFound: () => new Response("Not found", { status: 404 }),
2367
+ signatureTimeWindow: { minutes: 5 },
2368
+ skipSignatureVerification: false,
2369
+ inboxChallengePolicy: { enabled: true }
2370
+ });
2371
+ assertEquals(response.status, 401);
2372
+ const acceptSig = response.headers.get("Accept-Signature");
2373
+ assert(acceptSig != null, "Accept-Signature header must be present when unverifiedActivityHandler returns undefined and challenge policy is enabled");
2374
+ const parsed = parseAcceptSignature(acceptSig);
2375
+ assert(parsed.length > 0, "Accept-Signature must have at least one entry");
2376
+ assertEquals(response.headers.get("Cache-Control"), "no-store", "Cache-Control: no-store must be set for challenge-response");
2377
+ assertEquals(response.headers.get("Vary"), "Accept, Signature", "Vary header must include Accept and Signature");
2378
+ });
2379
+ test("handleInbox() challenge policy enabled + unverifiedActivityHandler throws error", async () => {
2380
+ const activity = new Create({
2381
+ id: new URL("https://example.com/activities/challenge-throw"),
2382
+ actor: new URL("https://example.com/person2"),
2383
+ object: new Note({
2384
+ id: new URL("https://example.com/notes/challenge-throw"),
2385
+ attribution: new URL("https://example.com/person2"),
2386
+ content: "Hello!"
2387
+ })
2388
+ });
2389
+ const originalRequest = new Request("https://example.com/", {
2390
+ method: "POST",
2391
+ body: JSON.stringify(await activity.toJsonLd())
2392
+ });
2393
+ const signedRequest = await signRequest(originalRequest, rsaPrivateKey3, rsaPublicKey3.id);
2394
+ const jsonLd = await activity.toJsonLd();
2395
+ const tamperedBody = JSON.stringify({
2396
+ ...jsonLd,
2397
+ "https://example.com/tampered": true
2398
+ });
2399
+ const tamperedRequest = new Request(signedRequest.url, {
2400
+ method: signedRequest.method,
2401
+ headers: signedRequest.headers,
2402
+ body: tamperedBody
2403
+ });
2404
+ const federation = createFederation({ kv: new MemoryKvStore() });
2405
+ const context = createRequestContext({
2406
+ federation,
2407
+ request: tamperedRequest,
2408
+ url: new URL(tamperedRequest.url),
2409
+ data: void 0,
2410
+ documentLoader: mockDocumentLoader
2411
+ });
2412
+ const actorDispatcher = (_ctx, identifier) => {
2413
+ if (identifier !== "someone") return null;
2414
+ return new Person({ name: "Someone" });
2415
+ };
2416
+ const kv = new MemoryKvStore();
2417
+ const response = await handleInbox(tamperedRequest, {
2418
+ recipient: "someone",
2419
+ context,
2420
+ inboxContextFactory(_activity) {
2421
+ return createInboxContext({
2422
+ ...context,
2423
+ clone: void 0,
2424
+ recipient: "someone"
2425
+ });
2426
+ },
2427
+ kv,
2428
+ kvPrefixes: {
2429
+ activityIdempotence: ["_fedify", "activityIdempotence"],
2430
+ publicKey: ["_fedify", "publicKey"],
2431
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
2432
+ },
2433
+ actorDispatcher,
2434
+ unverifiedActivityHandler() {
2435
+ throw new Error("handler error");
2436
+ },
2437
+ onNotFound: () => new Response("Not found", { status: 404 }),
2438
+ signatureTimeWindow: { minutes: 5 },
2439
+ skipSignatureVerification: false,
2440
+ inboxChallengePolicy: { enabled: true }
2441
+ });
2442
+ assertEquals(response.status, 401);
2443
+ const acceptSig = response.headers.get("Accept-Signature");
2444
+ assert(acceptSig != null, "Accept-Signature header must be present when unverifiedActivityHandler throws and challenge policy is enabled");
2445
+ const parsed = parseAcceptSignature(acceptSig);
2446
+ assert(parsed.length > 0, "Accept-Signature must have at least one entry");
2447
+ assertEquals(response.headers.get("Cache-Control"), "no-store", "Cache-Control: no-store must be set for challenge-response");
2448
+ assertEquals(response.headers.get("Vary"), "Accept, Signature", "Vary header must include Accept and Signature");
2449
+ });
1670
2450
 
1671
2451
  //#endregion