@fedify/fedify 2.0.36-dev.2551 → 2.0.36-dev.2637

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 (61) hide show
  1. package/dist/{builder-Dndt590h.mjs → builder-BC3eRIbe.mjs} +3 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{context-lxjaUTAI.d.ts → context-C00pWP8x.d.ts} +6 -0
  6. package/dist/{context-CnoWxuoB.d.cts → context-ao-qb3eP.d.cts} +6 -0
  7. package/dist/{deno-D_rTMbAD.mjs → deno-Dv6PIwH_.mjs} +1 -1
  8. package/dist/{docloader-DpTrZ5sA.mjs → docloader-BgHM-JLc.mjs} +2 -2
  9. package/dist/federation/builder.test.mjs +1 -1
  10. package/dist/federation/handler.test.mjs +8 -8
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/inbox.test.mjs +1 -1
  13. package/dist/federation/middleware.test.mjs +696 -8
  14. package/dist/federation/mod.cjs +1 -1
  15. package/dist/federation/mod.d.cts +2 -2
  16. package/dist/federation/mod.d.ts +2 -2
  17. package/dist/federation/mod.js +1 -1
  18. package/dist/federation/send.test.mjs +3 -3
  19. package/dist/federation/temporal.test.mjs +1 -1
  20. package/dist/federation/webfinger.test.mjs +1 -1
  21. package/dist/{http-DGD9d8-A.mjs → http-BWoAqky4.mjs} +2 -2
  22. package/dist/{http-BS1CmlZ6.js → http-CrEB4I8A.js} +1 -1
  23. package/dist/{http-DbDnj-Q7.cjs → http-D69BMfAn.cjs} +1 -1
  24. package/dist/{inbox-BwpBTbhH.mjs → inbox-0Dbv0QVc.mjs} +1 -1
  25. package/dist/{key-1Gf85RN1.mjs → key-ChKDZfvS.mjs} +1 -1
  26. package/dist/{kv-cache-BdA7HpfF.cjs → kv-cache-B0JZ30EF.cjs} +1 -1
  27. package/dist/{kv-cache-BARiicPE.js → kv-cache-CDvE8__9.js} +1 -1
  28. package/dist/{ld-BuuOR_Ms.mjs → ld-DbSNiv8Q.mjs} +99 -10
  29. package/dist/{middleware-C5Nypx4Z.mjs → middleware-BA9cM-vz.mjs} +1 -1
  30. package/dist/{middleware-DDM0j-X6.mjs → middleware-C849kmHB.mjs} +160 -29
  31. package/dist/{middleware-CQRCnFB4.js → middleware-CEufJR5k.js} +152 -21
  32. package/dist/{middleware-DThg01d8.cjs → middleware-C_fDk_dx.cjs} +153 -22
  33. package/dist/{middleware-D9TWYyT_.cjs → middleware-DCtCNota.cjs} +1 -1
  34. package/dist/{mod-w1oyHseQ.d.cts → mod-6uJWdCc0.d.cts} +1 -1
  35. package/dist/{mod-DVPS4yam.d.cts → mod-BA6vqC2S.d.cts} +19 -7
  36. package/dist/{mod-BS-jg3Wx.d.ts → mod-CL9Ep0uo.d.ts} +19 -7
  37. package/dist/{mod-I-34OMN3.d.ts → mod-DtRh015i.d.ts} +1 -1
  38. package/dist/mod.cjs +4 -4
  39. package/dist/mod.d.cts +3 -3
  40. package/dist/mod.d.ts +3 -3
  41. package/dist/mod.js +4 -4
  42. package/dist/nodeinfo/handler.test.mjs +1 -1
  43. package/dist/{owner-BI33UeIv.mjs → owner-BJeJfxnT.mjs} +2 -2
  44. package/dist/{proof-GM73LJjx.mjs → proof-CaMBdk_x.mjs} +36 -12
  45. package/dist/{proof-vVwkNRv4.cjs → proof-DNgQwz9E.cjs} +131 -18
  46. package/dist/{proof-BBHUW8ev.js → proof-DwooE8kV.js} +132 -19
  47. package/dist/{send-nImUK91D.mjs → send-CYrAOC1K.mjs} +2 -2
  48. package/dist/sig/http.test.mjs +2 -2
  49. package/dist/sig/key.test.mjs +2 -2
  50. package/dist/sig/ld.test.mjs +198 -4
  51. package/dist/sig/mod.cjs +2 -2
  52. package/dist/sig/mod.d.cts +1 -1
  53. package/dist/sig/mod.d.ts +1 -1
  54. package/dist/sig/mod.js +2 -2
  55. package/dist/sig/owner.test.mjs +1 -1
  56. package/dist/sig/proof.test.mjs +71 -2
  57. package/dist/{temporal-CfNFB7M_.mjs → temporal-BJoGKOXy.mjs} +1 -1
  58. package/dist/utils/docloader.test.mjs +2 -2
  59. package/dist/utils/mod.cjs +1 -1
  60. package/dist/utils/mod.js +1 -1
  61. package/package.json +5 -5
@@ -9,18 +9,19 @@ import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.m
9
9
  import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
10
10
  import { t as assertNotEquals } from "../assert_not_equals-BkxW5QJI.mjs";
11
11
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
12
- import { c as signRequest, u as verifyRequest } from "../http-DGD9d8-A.mjs";
12
+ import { c as signRequest, u as verifyRequest } from "../http-BWoAqky4.mjs";
13
13
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CCxRJhWa.mjs";
14
- import { n as getAuthenticatedDocumentLoader } from "../docloader-DpTrZ5sA.mjs";
15
- import { a as compactJsonLd, f as signJsonLd, m as verifyJsonLd, s as detachSignature } from "../ld-BuuOR_Ms.mjs";
16
- import { t as doesActorOwnKey } from "../owner-BI33UeIv.mjs";
17
- import { n as signObject, r as verifyObject } from "../proof-GM73LJjx.mjs";
14
+ import { n as getAuthenticatedDocumentLoader } from "../docloader-BgHM-JLc.mjs";
15
+ import { a as compactJsonLd, f as signJsonLd, i as attachSignature, m as verifyJsonLd, s as detachSignature } from "../ld-DbSNiv8Q.mjs";
16
+ import { t as doesActorOwnKey } from "../owner-BJeJfxnT.mjs";
17
+ import { n as signObject, r as verifyObject } from "../proof-CaMBdk_x.mjs";
18
18
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
19
- import { a as createFederation, i as KvSpecDeterminer, n as FederationImpl, r as InboxContextImpl, t as ContextImpl } from "../middleware-DDM0j-X6.mjs";
19
+ import { a as createFederation, i as KvSpecDeterminer, n as FederationImpl, r as InboxContextImpl, t as ContextImpl } from "../middleware-C849kmHB.mjs";
20
20
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
21
21
  import * as vocab from "@fedify/vocab";
22
22
  import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
23
23
  import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
24
+ import { configure, reset } from "@logtape/logtape";
24
25
  import dns from "node:dns/promises";
25
26
  //#region ../fixture/src/fixtures/example.com/create.json
26
27
  var id$2 = "https://example.com/create";
@@ -1575,6 +1576,320 @@ test("FederationImpl.sendActivity()", async (t) => {
1575
1576
  });
1576
1577
  esm_default.hardReset();
1577
1578
  });
1579
+ const QUEUE_STARTUP_WARNING = "Failed to start queue workers automatically: {error}";
1580
+ /**
1581
+ * Captures unhandled promise rejections for the duration of a test step.
1582
+ *
1583
+ * Deno dispatches `unhandledrejection` on `globalThis`, while Node.js and Bun
1584
+ * dispatch `unhandledRejection` on `process`. Bun's test runner currently
1585
+ * fires neither, so there the native runner's own unhandled-rejection failure
1586
+ * remains the signal; the assertion is harmless otherwise.
1587
+ */
1588
+ function captureUnhandledRejections() {
1589
+ const reasons = [];
1590
+ const isDeno = "Deno" in globalThis;
1591
+ const onRejectionEvent = (event) => {
1592
+ event.preventDefault();
1593
+ reasons.push(event.reason);
1594
+ };
1595
+ const onProcessRejection = (reason) => {
1596
+ reasons.push(reason);
1597
+ };
1598
+ const globalTarget = globalThis;
1599
+ const proc = globalThis.process;
1600
+ if (isDeno) globalTarget.addEventListener?.("unhandledrejection", onRejectionEvent);
1601
+ else proc?.on?.("unhandledRejection", onProcessRejection);
1602
+ async function settle() {
1603
+ await new Promise((resolve) => setTimeout(resolve, 0));
1604
+ await new Promise((resolve) => setTimeout(resolve, 0));
1605
+ }
1606
+ return {
1607
+ reasons,
1608
+ settle,
1609
+ stop() {
1610
+ if (isDeno) globalTarget.removeEventListener?.("unhandledrejection", onRejectionEvent);
1611
+ else proc?.off?.("unhandledRejection", onProcessRejection);
1612
+ }
1613
+ };
1614
+ }
1615
+ async function configureQueueStartupWarningCapture(records) {
1616
+ await configure({
1617
+ sinks: { buffer(record) {
1618
+ records.push(record);
1619
+ } },
1620
+ loggers: [{
1621
+ category: "fedify",
1622
+ sinks: ["buffer"],
1623
+ lowestLevel: "warning"
1624
+ }, {
1625
+ category: ["logtape", "meta"],
1626
+ sinks: []
1627
+ }],
1628
+ reset: true
1629
+ });
1630
+ }
1631
+ function queueStartupWarnings(records, error) {
1632
+ return records.filter((record) => record.level === "warning" && record.category.length === 3 && record.category[0] === "fedify" && record.category[1] === "federation" && record.category[2] === "queue" && record.rawMessage === QUEUE_STARTUP_WARNING && record.properties.error === error);
1633
+ }
1634
+ test("Federation.fetch() reports automatic queue startup failures", async (t) => {
1635
+ const listenError = /* @__PURE__ */ new TypeError("simulated listen() failure");
1636
+ const queue = {
1637
+ enqueue() {
1638
+ return Promise.resolve();
1639
+ },
1640
+ listen() {
1641
+ throw listenError;
1642
+ }
1643
+ };
1644
+ const capture = captureUnhandledRejections();
1645
+ const records = [];
1646
+ await configureQueueStartupWarningCapture(records);
1647
+ try {
1648
+ const federation = createFederation({
1649
+ kv: new MemoryKvStore(),
1650
+ queue,
1651
+ skipSignatureVerification: true,
1652
+ documentLoaderFactory: () => mockDocumentLoader,
1653
+ contextLoaderFactory: () => mockDocumentLoader
1654
+ });
1655
+ federation.setActorDispatcher("/actors/{identifier}", () => new Person({ id: new URL("https://example.com/actor") }));
1656
+ federation.setInboxListeners("/actors/{identifier}/inbox", "/inbox").on(Create, () => {});
1657
+ const post = (id) => federation.fetch(new Request("https://example.com/inbox", {
1658
+ method: "POST",
1659
+ headers: { "Content-Type": "application/activity+json" },
1660
+ body: JSON.stringify({
1661
+ "@context": "https://www.w3.org/ns/activitystreams",
1662
+ id,
1663
+ type: "Create",
1664
+ actor: "https://example.com/actor"
1665
+ })
1666
+ }), { contextData: void 0 });
1667
+ await t.step("synchronous failure", async () => {
1668
+ assertEquals((await post("https://example.com/activities/queued-1")).status, 202);
1669
+ await capture.settle();
1670
+ assertEquals(queueStartupWarnings(records, listenError).length, 1);
1671
+ assertEquals(capture.reasons, []);
1672
+ assertEquals((await post("https://example.com/activities/queued-2")).status, 202);
1673
+ await capture.settle();
1674
+ assertEquals(queueStartupWarnings(records, listenError).length, 1, "the warning must be logged only once per federation instance");
1675
+ assertEquals(capture.reasons, []);
1676
+ });
1677
+ await t.step("asynchronous failure", async () => {
1678
+ const asyncError = /* @__PURE__ */ new TypeError("simulated async listen() failure");
1679
+ let rejectListen;
1680
+ const asyncFederation = createFederation({
1681
+ kv: new MemoryKvStore(),
1682
+ queue: {
1683
+ enqueue() {
1684
+ return Promise.resolve();
1685
+ },
1686
+ listen() {
1687
+ return new Promise((_resolve, reject) => {
1688
+ rejectListen = reject;
1689
+ });
1690
+ }
1691
+ },
1692
+ skipSignatureVerification: true,
1693
+ documentLoaderFactory: () => mockDocumentLoader,
1694
+ contextLoaderFactory: () => mockDocumentLoader
1695
+ });
1696
+ asyncFederation.setActorDispatcher("/actors/{identifier}", () => new Person({ id: new URL("https://example.com/actor") }));
1697
+ asyncFederation.setInboxListeners("/actors/{identifier}/inbox", "/inbox").on(Create, () => {});
1698
+ assertEquals((await asyncFederation.fetch(new Request("https://example.com/inbox", {
1699
+ method: "POST",
1700
+ headers: { "Content-Type": "application/activity+json" },
1701
+ body: JSON.stringify({
1702
+ "@context": "https://www.w3.org/ns/activitystreams",
1703
+ id: "https://example.com/activities/queued-async",
1704
+ type: "Create",
1705
+ actor: "https://example.com/actor"
1706
+ })
1707
+ }), { contextData: void 0 })).status, 202);
1708
+ await capture.settle();
1709
+ assertEquals(queueStartupWarnings(records, asyncError).length, 0);
1710
+ assert(rejectListen != null);
1711
+ rejectListen(asyncError);
1712
+ await capture.settle();
1713
+ assertEquals(queueStartupWarnings(records, asyncError).length, 1, "a listener that rejects after startup must still be reported");
1714
+ assertEquals(capture.reasons, []);
1715
+ });
1716
+ } finally {
1717
+ await capture.settle();
1718
+ capture.stop();
1719
+ await reset();
1720
+ }
1721
+ });
1722
+ test("Federation.startQueue() rejects when a queue cannot listen", async () => {
1723
+ const listenError = /* @__PURE__ */ new TypeError("simulated listen() failure");
1724
+ const federation = createFederation({
1725
+ kv: new MemoryKvStore(),
1726
+ queue: {
1727
+ enqueue() {
1728
+ return Promise.resolve();
1729
+ },
1730
+ listen() {
1731
+ throw listenError;
1732
+ }
1733
+ },
1734
+ manuallyStartQueue: true
1735
+ });
1736
+ assertStrictEquals(await assertRejects(() => federation.startQueue(void 0)), listenError);
1737
+ });
1738
+ test("Federation.startQueue() keeps other queue workers running", async () => {
1739
+ const outboxError = /* @__PURE__ */ new TypeError("simulated outbox listen() failure");
1740
+ const lateInboxError = /* @__PURE__ */ new TypeError("simulated late inbox failure");
1741
+ let rejectInbox;
1742
+ let fanoutStarted = false;
1743
+ const inbox = {
1744
+ enqueue() {
1745
+ return Promise.resolve();
1746
+ },
1747
+ listen() {
1748
+ return new Promise((_resolve, reject) => {
1749
+ rejectInbox = reject;
1750
+ });
1751
+ }
1752
+ };
1753
+ const outbox = {
1754
+ enqueue() {
1755
+ return Promise.resolve();
1756
+ },
1757
+ listen() {
1758
+ throw outboxError;
1759
+ }
1760
+ };
1761
+ const fanout = {
1762
+ enqueue() {
1763
+ return Promise.resolve();
1764
+ },
1765
+ listen() {
1766
+ fanoutStarted = true;
1767
+ return new Promise(() => {});
1768
+ }
1769
+ };
1770
+ const capture = captureUnhandledRejections();
1771
+ try {
1772
+ const federation = createFederation({
1773
+ kv: new MemoryKvStore(),
1774
+ queue: {
1775
+ inbox,
1776
+ outbox,
1777
+ fanout
1778
+ },
1779
+ manuallyStartQueue: true
1780
+ });
1781
+ assertStrictEquals(await assertRejects(() => federation.startQueue(void 0)), outboxError);
1782
+ assert(fanoutStarted, "a synchronous throw from one queue must not prevent later queues from starting");
1783
+ assert(rejectInbox != null);
1784
+ rejectInbox(lateInboxError);
1785
+ await capture.settle();
1786
+ assertEquals(capture.reasons, [], "an earlier listener rejection must remain handled after startup fails");
1787
+ } finally {
1788
+ await capture.settle();
1789
+ capture.stop();
1790
+ }
1791
+ });
1792
+ test("ContextImpl.sendActivity() reports automatic queue startup failures", async (t) => {
1793
+ const sender = [{
1794
+ privateKey: ed25519PrivateKey,
1795
+ keyId: ed25519Multikey.id
1796
+ }];
1797
+ const recipient = {
1798
+ id: new URL("https://example.com/recipient"),
1799
+ inboxId: new URL("https://example.com/inbox")
1800
+ };
1801
+ const createActivity = () => new Create({
1802
+ id: new URL("https://example.com/activities/queue-startup-send"),
1803
+ actor: new URL("https://example.com/actor")
1804
+ });
1805
+ await t.step("outbox", async () => {
1806
+ const listenError = /* @__PURE__ */ new TypeError("simulated outbox listen() failure");
1807
+ const messages = [];
1808
+ const queue = {
1809
+ enqueue(message) {
1810
+ messages.push(message);
1811
+ return Promise.resolve();
1812
+ },
1813
+ listen() {
1814
+ throw listenError;
1815
+ }
1816
+ };
1817
+ const capture = captureUnhandledRejections();
1818
+ const records = [];
1819
+ await configureQueueStartupWarningCapture(records);
1820
+ try {
1821
+ await new ContextImpl({
1822
+ data: void 0,
1823
+ federation: new FederationImpl({
1824
+ kv: new MemoryKvStore(),
1825
+ queue: { outbox: queue },
1826
+ documentLoaderFactory: () => mockDocumentLoader,
1827
+ contextLoaderFactory: () => mockDocumentLoader
1828
+ }),
1829
+ url: new URL("https://example.com/"),
1830
+ documentLoader: mockDocumentLoader,
1831
+ contextLoader: mockDocumentLoader
1832
+ }).sendActivity(sender, recipient, createActivity());
1833
+ assertEquals(messages.map((message) => message.type), ["outbox"]);
1834
+ await capture.settle();
1835
+ assertEquals(queueStartupWarnings(records, listenError).length, 1);
1836
+ assertEquals(capture.reasons, []);
1837
+ } finally {
1838
+ await capture.settle();
1839
+ capture.stop();
1840
+ await reset();
1841
+ }
1842
+ });
1843
+ await t.step("fanout", async () => {
1844
+ const listenError = /* @__PURE__ */ new TypeError("simulated fanout listen() failure");
1845
+ const messages = [];
1846
+ const outbox = {
1847
+ enqueue() {
1848
+ return Promise.resolve();
1849
+ },
1850
+ listen() {
1851
+ return Promise.resolve();
1852
+ }
1853
+ };
1854
+ const fanout = {
1855
+ enqueue(message) {
1856
+ messages.push(message);
1857
+ return Promise.resolve();
1858
+ },
1859
+ listen() {
1860
+ throw listenError;
1861
+ }
1862
+ };
1863
+ const capture = captureUnhandledRejections();
1864
+ const records = [];
1865
+ await configureQueueStartupWarningCapture(records);
1866
+ try {
1867
+ await new ContextImpl({
1868
+ data: void 0,
1869
+ federation: new FederationImpl({
1870
+ kv: new MemoryKvStore(),
1871
+ queue: {
1872
+ outbox,
1873
+ fanout
1874
+ },
1875
+ documentLoaderFactory: () => mockDocumentLoader,
1876
+ contextLoaderFactory: () => mockDocumentLoader
1877
+ }),
1878
+ url: new URL("https://example.com/"),
1879
+ documentLoader: mockDocumentLoader,
1880
+ contextLoader: mockDocumentLoader
1881
+ }).sendActivity(sender, recipient, createActivity(), { fanout: "force" });
1882
+ assertEquals(messages.map((message) => message.type), ["fanout"]);
1883
+ await capture.settle();
1884
+ assertEquals(queueStartupWarnings(records, listenError).length, 1);
1885
+ assertEquals(capture.reasons, []);
1886
+ } finally {
1887
+ await capture.settle();
1888
+ capture.stop();
1889
+ await reset();
1890
+ }
1891
+ });
1892
+ });
1578
1893
  test("FederationImpl.processQueuedTask()", async (t) => {
1579
1894
  await t.step("with MessageQueue having nativeRetrial", async () => {
1580
1895
  const kv = new MemoryKvStore();
@@ -3594,15 +3909,19 @@ test("ContextImpl.sendActivity()", async (t) => {
3594
3909
  queue
3595
3910
  });
3596
3911
  federation2.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
3597
- if (identifier !== "john") return null;
3912
+ if (identifier !== "john" && identifier !== "rsa-only") return null;
3598
3913
  const keys = await ctx.getActorKeyPairs(identifier);
3599
3914
  return new vocab.Person({
3600
3915
  id: ctx.getActorUri(identifier),
3601
- preferredUsername: "john",
3916
+ preferredUsername: identifier,
3602
3917
  publicKey: keys[0].cryptographicKey,
3603
3918
  assertionMethods: keys.map((k) => k.multikey)
3604
3919
  });
3605
3920
  }).setKeyPairsDispatcher((_ctx, identifier) => {
3921
+ if (identifier === "rsa-only") return [{
3922
+ privateKey: rsaPrivateKey2,
3923
+ publicKey: rsaPublicKey2.publicKey
3924
+ }];
3606
3925
  if (identifier !== "john") return [];
3607
3926
  return [{
3608
3927
  privateKey: rsaPrivateKey2,
@@ -3651,6 +3970,375 @@ test("ContextImpl.sendActivity()", async (t) => {
3651
3970
  proofCount++;
3652
3971
  }
3653
3972
  assertEquals(proofCount, 1);
3973
+ const oldContext = [
3974
+ "https://w3id.org/identity/v1",
3975
+ "https://www.w3.org/ns/activitystreams",
3976
+ "https://w3id.org/security/data-integrity/v1"
3977
+ ];
3978
+ const oldJson = await (await signObject(new vocab.Create({
3979
+ id: new URL("https://example.com/activity/1"),
3980
+ actor: new URL("https://example.com/john")
3981
+ }), ed25519PrivateKey, ed25519Multikey.id, {
3982
+ contextLoader: documentLoader,
3983
+ context: oldContext
3984
+ })).toJsonLd({
3985
+ format: "compact",
3986
+ contextLoader: documentLoader,
3987
+ context: oldContext
3988
+ });
3989
+ queue.clear();
3990
+ await federation2.processQueuedTask(void 0, {
3991
+ ...fanoutMsg,
3992
+ id: crypto.randomUUID(),
3993
+ activity: oldJson
3994
+ });
3995
+ const replayed = queue.messages.at(-1);
3996
+ assert(replayed != null && replayed.type === "outbox");
3997
+ assertEquals(detachSignature(replayed.activity), oldJson);
3998
+ const proofless = { ...oldJson };
3999
+ delete proofless.proof;
4000
+ queue.clear();
4001
+ await federation2.processQueuedTask(void 0, {
4002
+ ...fanoutMsg,
4003
+ id: crypto.randomUUID(),
4004
+ activity: proofless
4005
+ });
4006
+ const signedReplay = queue.messages.at(-1);
4007
+ assert(signedReplay != null && signedReplay.type === "outbox");
4008
+ assert(typeof signedReplay.activity === "object" && signedReplay.activity != null && "proof" in signedReplay.activity);
4009
+ const edKey = (await ctx2.getActorKeyPairs("john")).find((key) => key.privateKey.algorithm.name === "Ed25519");
4010
+ assert(edKey != null && edKey.multikey.id != null);
4011
+ const restoredJson = await (await signObject(new vocab.Create({
4012
+ id: new URL("https://example.com/activity/old"),
4013
+ actor: ctx2.getActorUri("john")
4014
+ }), edKey.privateKey, edKey.multikey.id, {
4015
+ contextLoader: documentLoader,
4016
+ context: oldContext
4017
+ })).toJsonLd({
4018
+ format: "compact",
4019
+ contextLoader: documentLoader,
4020
+ context: oldContext
4021
+ });
4022
+ const restored = await vocab.Create.fromJsonLd(restoredJson, {
4023
+ contextLoader: documentLoader,
4024
+ documentLoader
4025
+ });
4026
+ queue.clear();
4027
+ await ctx2.sendActivity([{
4028
+ privateKey: rsaPrivateKey2,
4029
+ keyId: rsaPublicKey2.id
4030
+ }], {
4031
+ id: new URL("https://example.com/recipient"),
4032
+ inboxId: new URL("https://example.com/inbox")
4033
+ }, restored);
4034
+ const resent = queue.messages.at(-1);
4035
+ assert(resent != null && resent.type === "outbox");
4036
+ assertEquals(detachSignature(resent.activity), restoredJson);
4037
+ queue.clear();
4038
+ await ctx2.sendActivity([{
4039
+ privateKey: rsaPrivateKey2,
4040
+ keyId: rsaPublicKey2.id
4041
+ }], {
4042
+ id: new URL("https://example.com/recipient"),
4043
+ inboxId: new URL("https://example.com/inbox")
4044
+ }, restored, { fanout: "force" });
4045
+ const queuedFanout = queue.messages.at(-1);
4046
+ assert(queuedFanout != null && queuedFanout.type === "fanout");
4047
+ assertEquals(queuedFanout.activity, restoredJson);
4048
+ const restoredWithSignature = await vocab.Create.fromJsonLd(attachSignature(restoredJson, {
4049
+ "@context": "https://w3id.org/identity/v1",
4050
+ type: "RsaSignature2017",
4051
+ creator: "https://example.com/person2#key4",
4052
+ created: "2026-01-01T00:00:00Z",
4053
+ signatureValue: "abc"
4054
+ }), {
4055
+ contextLoader: documentLoader,
4056
+ documentLoader
4057
+ });
4058
+ queue.clear();
4059
+ await ctx2.sendActivity([{
4060
+ privateKey: rsaPrivateKey2,
4061
+ keyId: rsaPublicKey2.id
4062
+ }], {
4063
+ id: new URL("https://example.com/recipient"),
4064
+ inboxId: new URL("https://example.com/inbox")
4065
+ }, restoredWithSignature);
4066
+ const resigned = queue.messages.at(-1);
4067
+ assert(resigned != null && resigned.type === "outbox");
4068
+ assertEquals(detachSignature(resigned.activity), restoredJson);
4069
+ const rsaOnlyJson = await (await signObject(new vocab.Create({
4070
+ id: new URL("https://example.com/activity/rsa-only"),
4071
+ actor: new URL("https://example.com/rsa-only")
4072
+ }), ed25519PrivateKey, ed25519Multikey.id, {
4073
+ contextLoader: documentLoader,
4074
+ context: oldContext
4075
+ })).toJsonLd({
4076
+ format: "compact",
4077
+ contextLoader: documentLoader,
4078
+ context: oldContext
4079
+ });
4080
+ const rsaOnlyRestored = await vocab.Create.fromJsonLd(rsaOnlyJson, {
4081
+ contextLoader: documentLoader,
4082
+ documentLoader
4083
+ });
4084
+ queue.clear();
4085
+ await ctx2.sendActivity({ identifier: "rsa-only" }, {
4086
+ id: new URL("https://example.com/recipient"),
4087
+ inboxId: new URL("https://example.com/inbox")
4088
+ }, rsaOnlyRestored, { fanout: "force" });
4089
+ const rsaOnlyMessage = queue.messages.at(-1);
4090
+ assert(rsaOnlyMessage != null && rsaOnlyMessage.type === "fanout");
4091
+ assertEquals(rsaOnlyMessage.activity, rsaOnlyJson);
4092
+ queue.clear();
4093
+ await ctx2.sendActivity({ username: "john" }, {
4094
+ id: new URL("https://example.com/recipient"),
4095
+ inboxId: new URL("https://example.com/inbox")
4096
+ }, restored, { fanout: "force" });
4097
+ const namedFanout = queue.messages.at(-1);
4098
+ assert(namedFanout != null && namedFanout.type === "fanout");
4099
+ const namedJson = detachSignature(namedFanout.activity);
4100
+ assertEquals((Array.isArray(namedJson.proof) ? namedJson.proof : [namedJson.proof]).length, 1);
4101
+ assert(namedJson["@context"].includes("https://w3id.org/security/v1"));
4102
+ const noIdJson = await (await signObject(new vocab.Create({ actor: ctx2.getActorUri("john") }), edKey.privateKey, edKey.multikey.id, {
4103
+ contextLoader: documentLoader,
4104
+ context: oldContext
4105
+ })).toJsonLd({
4106
+ format: "compact",
4107
+ contextLoader: documentLoader,
4108
+ context: oldContext
4109
+ });
4110
+ const noIdRestored = await vocab.Create.fromJsonLd(noIdJson, {
4111
+ contextLoader: documentLoader,
4112
+ documentLoader
4113
+ });
4114
+ queue.clear();
4115
+ await ctx2.sendActivity([{
4116
+ privateKey: rsaPrivateKey2,
4117
+ keyId: rsaPublicKey2.id
4118
+ }], {
4119
+ id: new URL("https://example.com/recipient"),
4120
+ inboxId: new URL("https://example.com/inbox")
4121
+ }, noIdRestored);
4122
+ const noIdQueued = queue.messages.at(-1);
4123
+ assert(noIdQueued != null && noIdQueued.type === "outbox");
4124
+ assert(typeof detachSignature(noIdQueued.activity).id === "string");
4125
+ const currentJson = await (await signObject(new vocab.Create({
4126
+ id: new URL("https://example.com/activity/current"),
4127
+ actor: ctx2.getActorUri("john")
4128
+ }), edKey.privateKey, edKey.multikey.id, { contextLoader: documentLoader })).toJsonLd({
4129
+ format: "compact",
4130
+ contextLoader: documentLoader
4131
+ });
4132
+ const currentRestored = await vocab.Create.fromJsonLd(currentJson, {
4133
+ contextLoader: documentLoader,
4134
+ documentLoader
4135
+ });
4136
+ queue.clear();
4137
+ await ctx2.sendActivity({ username: "john" }, {
4138
+ id: new URL("https://example.com/recipient"),
4139
+ inboxId: new URL("https://example.com/inbox")
4140
+ }, currentRestored, { fanout: "force" });
4141
+ const currentFanout = queue.messages.at(-1);
4142
+ assert(currentFanout != null && currentFanout.type === "fanout");
4143
+ const currentQueued = detachSignature(currentFanout.activity);
4144
+ assertEquals((Array.isArray(currentQueued.proof) ? currentQueued.proof : [currentQueued.proof]).length, 2);
4145
+ });
4146
+ queue.clear();
4147
+ await t.step("activity transformers preserve proofs", async () => {
4148
+ const originalProof = new vocab.DataIntegrityProof({
4149
+ cryptosuite: "eddsa-jcs-2022",
4150
+ verificationMethod: new URL("https://example.com/rsa-only#multikey-2"),
4151
+ proofPurpose: "assertionMethod",
4152
+ proofValue: new Uint8Array([
4153
+ 1,
4154
+ 2,
4155
+ 3,
4156
+ 4
4157
+ ]),
4158
+ created: Temporal.Instant.from("2026-01-01T00:00:00Z")
4159
+ });
4160
+ const addedProof = new vocab.DataIntegrityProof({
4161
+ cryptosuite: "eddsa-jcs-2022",
4162
+ verificationMethod: new URL("https://example.com/rsa-only#multikey-2"),
4163
+ proofPurpose: "assertionMethod",
4164
+ proofValue: new Uint8Array([
4165
+ 5,
4166
+ 6,
4167
+ 7,
4168
+ 8
4169
+ ]),
4170
+ created: Temporal.Instant.from("2026-01-02T00:00:00Z")
4171
+ });
4172
+ const replacementProof = new vocab.DataIntegrityProof({
4173
+ cryptosuite: "eddsa-jcs-2022",
4174
+ verificationMethod: new URL("https://example.com/rsa-only#multikey-2"),
4175
+ proofPurpose: "assertionMethod",
4176
+ proofValue: new Uint8Array([
4177
+ 9,
4178
+ 10,
4179
+ 11,
4180
+ 12
4181
+ ]),
4182
+ created: Temporal.Instant.from("2026-01-03T00:00:00Z")
4183
+ });
4184
+ const restoredProof = new vocab.DataIntegrityProof({
4185
+ cryptosuite: "eddsa-jcs-2022",
4186
+ verificationMethod: new URL("https://example.com/rsa-only#multikey-2"),
4187
+ proofPurpose: "assertionMethod",
4188
+ proofValue: new Uint8Array([
4189
+ 13,
4190
+ 14,
4191
+ 15,
4192
+ 16
4193
+ ]),
4194
+ created: Temporal.Instant.from("2026-01-04T00:00:00Z")
4195
+ });
4196
+ const restoredProofHolder = {};
4197
+ const federation3 = new FederationImpl({
4198
+ kv,
4199
+ allowPrivateAddress: true,
4200
+ queue,
4201
+ activityTransformers: [(activity) => {
4202
+ if (activity.id?.href === "https://example.com/activity/transformed-proof") return activity.clone({ proofs: [originalProof, addedProof] });
4203
+ if (activity.id?.href === "https://example.com/activity/replaced-proof") return new vocab.Create({
4204
+ id: activity.id,
4205
+ actor: new URL("https://example.com/rsa-only"),
4206
+ object: new URL("https://example.com/note/replaced"),
4207
+ proofs: [replacementProof]
4208
+ });
4209
+ if (activity.id?.href === "https://example.com/activity/restored-proof") return activity.clone({ proofs: [restoredProofHolder.old, restoredProof] });
4210
+ return activity;
4211
+ }]
4212
+ });
4213
+ federation3.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
4214
+ if (identifier !== "rsa-only") return null;
4215
+ const keys = await ctx.getActorKeyPairs(identifier);
4216
+ return new vocab.Person({
4217
+ id: ctx.getActorUri(identifier),
4218
+ preferredUsername: identifier,
4219
+ publicKey: keys[0].cryptographicKey,
4220
+ assertionMethods: keys.map((k) => k.multikey)
4221
+ });
4222
+ }).setKeyPairsDispatcher((_ctx, identifier) => identifier === "rsa-only" ? [{
4223
+ privateKey: rsaPrivateKey2,
4224
+ publicKey: rsaPublicKey2.publicKey
4225
+ }] : []);
4226
+ const ctx3 = new ContextImpl({
4227
+ data: void 0,
4228
+ federation: federation3,
4229
+ url: new URL("https://example.com/"),
4230
+ documentLoader,
4231
+ contextLoader: documentLoader
4232
+ });
4233
+ const activity = new vocab.Create({
4234
+ id: new URL("https://example.com/activity/transformed-proof"),
4235
+ actor: ctx3.getActorUri("rsa-only"),
4236
+ proofs: [originalProof]
4237
+ });
4238
+ await ctx3.sendActivity({ identifier: "rsa-only" }, {
4239
+ id: new URL("https://example.com/recipient"),
4240
+ inboxId: new URL("https://example.com/inbox")
4241
+ }, activity, { fanout: "force" });
4242
+ const fanout = queue.messages.at(-1);
4243
+ assert(fanout != null && fanout.type === "fanout");
4244
+ const fanoutJson = detachSignature(fanout.activity);
4245
+ assertEquals((Array.isArray(fanoutJson.proof) ? fanoutJson.proof : [fanoutJson.proof]).length, 2);
4246
+ const replacedActivity = new vocab.Create({
4247
+ id: new URL("https://example.com/activity/replaced-proof"),
4248
+ actor: ctx3.getActorUri("rsa-only"),
4249
+ proofs: [originalProof]
4250
+ });
4251
+ queue.clear();
4252
+ await ctx3.sendActivity({ identifier: "rsa-only" }, {
4253
+ id: new URL("https://example.com/recipient"),
4254
+ inboxId: new URL("https://example.com/inbox")
4255
+ }, replacedActivity, { fanout: "force" });
4256
+ const replacedFanout = queue.messages.at(-1);
4257
+ assert(replacedFanout != null && replacedFanout.type === "fanout");
4258
+ const replacedJson = detachSignature(replacedFanout.activity);
4259
+ assertEquals(replacedJson.object, "https://example.com/note/replaced");
4260
+ const replacedProofs = Array.isArray(replacedJson.proof) ? replacedJson.proof : [replacedJson.proof];
4261
+ assertEquals(replacedProofs.length, 1);
4262
+ const [keptProof] = replacedProofs;
4263
+ assertEquals(keptProof.created, "2026-01-03T00:00:00Z");
4264
+ const oldContext = [
4265
+ "https://w3id.org/identity/v1",
4266
+ "https://www.w3.org/ns/activitystreams",
4267
+ "https://w3id.org/security/data-integrity/v1"
4268
+ ];
4269
+ const restoredJson = await (await signObject(new vocab.Create({
4270
+ id: new URL("https://example.com/activity/restored-proof"),
4271
+ actor: ctx3.getActorUri("rsa-only")
4272
+ }), ed25519PrivateKey, ed25519Multikey.id, {
4273
+ contextLoader: documentLoader,
4274
+ context: oldContext
4275
+ })).toJsonLd({
4276
+ format: "compact",
4277
+ contextLoader: documentLoader,
4278
+ context: oldContext
4279
+ });
4280
+ const restored = await vocab.Create.fromJsonLd(restoredJson, {
4281
+ contextLoader: documentLoader,
4282
+ documentLoader
4283
+ });
4284
+ for await (const proof of restored.getProofs({ contextLoader: documentLoader })) {
4285
+ restoredProofHolder.old = proof;
4286
+ break;
4287
+ }
4288
+ queue.clear();
4289
+ await ctx3.sendActivity({ identifier: "rsa-only" }, {
4290
+ id: new URL("https://example.com/recipient"),
4291
+ inboxId: new URL("https://example.com/inbox")
4292
+ }, restored, { fanout: "force" });
4293
+ const restoredFanout = queue.messages.at(-1);
4294
+ assert(restoredFanout != null && restoredFanout.type === "fanout");
4295
+ const restoredFanoutJson = detachSignature(restoredFanout.activity);
4296
+ const restoredProofs = Array.isArray(restoredFanoutJson.proof) ? restoredFanoutJson.proof : [restoredFanoutJson.proof];
4297
+ assertEquals(restoredProofs.length, 1);
4298
+ const [transformedProof] = restoredProofs;
4299
+ assertEquals(transformedProof.created, "2026-01-04T00:00:00Z");
4300
+ assert(Array.isArray(restoredFanoutJson["@context"]) && restoredFanoutJson["@context"].includes("https://w3id.org/security/v1"));
4301
+ const staleTimestampJson = await (await signObject(new vocab.Create({
4302
+ id: new URL("https://example.com/activity/stale-timestamp"),
4303
+ actor: ctx3.getActorUri("rsa-only")
4304
+ }), ed25519PrivateKey, ed25519Multikey.id, {
4305
+ contextLoader: documentLoader,
4306
+ context: oldContext
4307
+ })).toJsonLd({
4308
+ format: "compact",
4309
+ contextLoader: documentLoader,
4310
+ context: oldContext
4311
+ });
4312
+ const staleTimestampProof = Array.isArray(staleTimestampJson.proof) ? staleTimestampJson.proof[0] : staleTimestampJson.proof;
4313
+ staleTimestampProof.created = "2026-01-01T00:00:00+00:00";
4314
+ const staleTimestamp = await vocab.Create.fromJsonLd(staleTimestampJson, {
4315
+ contextLoader: documentLoader,
4316
+ documentLoader
4317
+ });
4318
+ queue.clear();
4319
+ await ctx3.sendActivity({ identifier: "rsa-only" }, {
4320
+ id: new URL("https://example.com/recipient"),
4321
+ inboxId: new URL("https://example.com/inbox")
4322
+ }, staleTimestamp, { fanout: "force" });
4323
+ const staleTimestampFanout = queue.messages.at(-1);
4324
+ assert(staleTimestampFanout != null && staleTimestampFanout.type === "fanout");
4325
+ assertEquals(detachSignature(staleTimestampFanout.activity), staleTimestampJson);
4326
+ const proofless = new vocab.Create({
4327
+ id: new URL("https://example.com/activity/proofless"),
4328
+ actor: ctx3.getActorUri("rsa-only")
4329
+ });
4330
+ let unformattedSerializations = 0;
4331
+ const originalToJsonLd = proofless.toJsonLd.bind(proofless);
4332
+ Object.defineProperty(proofless, "toJsonLd", { value: async (options) => {
4333
+ if (options?.format == null) unformattedSerializations++;
4334
+ return await originalToJsonLd(options);
4335
+ } });
4336
+ queue.clear();
4337
+ await ctx3.sendActivity({ identifier: "rsa-only" }, {
4338
+ id: new URL("https://example.com/recipient"),
4339
+ inboxId: new URL("https://example.com/inbox")
4340
+ }, proofless, { fanout: "skip" });
4341
+ assertEquals(unformattedSerializations, 0);
3654
4342
  });
3655
4343
  queue.clear();
3656
4344
  await t.step("fanout: \"skip\"", async () => {