@fedify/fedify 2.5.0-dev.2597 → 2.5.0-dev.2614

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 (123) hide show
  1. package/dist/{builder-Da_V_LDj.mjs → builder-B2H8GsyI.mjs} +82 -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/{compound-proof-BqqP38Sf.mjs → compound-proof-BavXW-ow.mjs} +2 -2
  6. package/dist/{context-ByflxsY9.d.cts → context-C1NqcIct.d.cts} +1061 -443
  7. package/dist/{context-uQUY5fQT.d.ts → context-xrqq4Hrq.d.ts} +1064 -446
  8. package/dist/{deno-4cGKg59R.mjs → deno-CRXki9-m.mjs} +1 -1
  9. package/dist/{docloader-BiHD6NSO.mjs → docloader-Ca6i6ddp.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +4 -4
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/inbox-actorless.test.mjs +5 -5
  17. package/dist/federation/inbox-report.test.mjs +220 -5
  18. package/dist/federation/inbox-view.test.mjs +1 -1
  19. package/dist/federation/inbox.test.mjs +1 -1
  20. package/dist/federation/metrics.test.mjs +1 -1
  21. package/dist/federation/middleware.test.mjs +7 -7
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +3 -3
  24. package/dist/federation/mod.d.ts +4 -4
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +1 -1
  27. package/dist/federation/outbound-body.test.d.mts +2 -0
  28. package/dist/federation/outbound-body.test.mjs +149 -0
  29. package/dist/federation/outbound-capture.test.d.mts +2 -0
  30. package/dist/federation/outbound-capture.test.mjs +394 -0
  31. package/dist/federation/outbound-dispatch.test.d.mts +2 -0
  32. package/dist/federation/outbound-dispatch.test.mjs +368 -0
  33. package/dist/federation/outbound-document.test.d.mts +2 -0
  34. package/dist/federation/outbound-document.test.mjs +171 -0
  35. package/dist/federation/outbound-provenance.test.d.mts +2 -0
  36. package/dist/federation/outbound-provenance.test.mjs +543 -0
  37. package/dist/federation/outbound-report.test.d.mts +2 -0
  38. package/dist/federation/outbound-report.test.mjs +5475 -0
  39. package/dist/federation/outbound-workers.test.d.mts +2 -0
  40. package/dist/federation/outbound-workers.test.mjs +64 -0
  41. package/dist/federation/outbound-workflow.test.d.mts +2 -0
  42. package/dist/federation/outbound-workflow.test.mjs +154 -0
  43. package/dist/federation/outgoing-proof.test.mjs +4 -4
  44. package/dist/federation/portable-actor.test.mjs +3 -3
  45. package/dist/federation/portable-chain.test.mjs +3 -3
  46. package/dist/federation/portable-collection.test.mjs +4 -4
  47. package/dist/federation/portable-delivery.test.mjs +106 -7
  48. package/dist/federation/portable-gateway-keys.test.mjs +5 -5
  49. package/dist/federation/portable-inbox.test.mjs +430 -5
  50. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  51. package/dist/federation/portable-route-activity.test.mjs +2 -2
  52. package/dist/federation/portable.test.mjs +3 -3
  53. package/dist/federation/send.test.mjs +44 -3
  54. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  55. package/dist/federation/tasks/tasks.test.mjs +2 -2
  56. package/dist/federation/temporal.test.mjs +1 -1
  57. package/dist/federation/webfinger.test.mjs +1 -1
  58. package/dist/{federation-BKJZEC0O.cjs → federation-DRl81KbJ.cjs} +2358 -405
  59. package/dist/{http-DQP4rw46.js → http-C2H4DT6t.js} +62 -32
  60. package/dist/{http-KU7_TLD9.cjs → http-DHSIMipE.cjs} +62 -32
  61. package/dist/{http-D5n4S1oL.mjs → http-DqOXkYbX.mjs} +64 -34
  62. package/dist/{inbox-CElAglrC.mjs → inbox-C4iW1g0F.mjs} +90 -3
  63. package/dist/{inbox-view-BTUuIIf6.mjs → inbox-view-Bu_aSwFJ.mjs} +1 -1
  64. package/dist/{key-DbLNT-vA.d.ts → key-dwwAg5bA.d.ts} +1 -1
  65. package/dist/{kv-cache-CFBwLUZr.js → kv-cache-B-x_OrBo.js} +1 -1
  66. package/dist/{kv-cache-DrQn-ugb.cjs → kv-cache-CHR_BVyM.cjs} +1 -1
  67. package/dist/{kv-cache-BV55jAE6.mjs → kv-cache-DJsxu9nI.mjs} +1 -1
  68. package/dist/{ld-CmAQYDWE.mjs → ld-C5JOBBFj.mjs} +2 -2
  69. package/dist/{ld-Inagww4R.d.ts → ld-o4LnhCYY.d.ts} +2 -2
  70. package/dist/{metrics-BvV60TwE.mjs → metrics-CTteRnEr.mjs} +1 -1
  71. package/dist/{metrics-DoRtI0qU.d.ts → metrics-DVKYY6K1.d.ts} +1 -1
  72. package/dist/{middleware-Di4y_M3y.js → middleware-B-tMVSUA.js} +2303 -351
  73. package/dist/{middleware-D02eC0j7.mjs → middleware-BkRqMGPU.mjs} +1573 -325
  74. package/dist/{middleware-iAJcDIM2.mjs → middleware-CPigKLnt.mjs} +1 -1
  75. package/dist/{mod-B7j30IQf.d.ts → mod-CQC9xlLZ.d.ts} +3 -3
  76. package/dist/{mod-B4kBw1-8.d.ts → mod-D22ZsiKi.d.ts} +2 -2
  77. package/dist/{mod-BN0TTfyQ.d.ts → mod-DO0epJGH.d.ts} +1 -1
  78. package/dist/{mod-DF2gbN_d.d.cts → mod-TwiQgEf3.d.cts} +1 -1
  79. package/dist/mod.cjs +4 -4
  80. package/dist/mod.d.cts +4 -4
  81. package/dist/mod.d.ts +8 -8
  82. package/dist/mod.js +4 -4
  83. package/dist/nodeinfo/handler.test.mjs +1 -1
  84. package/dist/outbound-body-CckPA5gc.mjs +83 -0
  85. package/dist/outbound-capture-DVgnRzlf.mjs +184 -0
  86. package/dist/outbound-document-BMgwLXTY.mjs +179 -0
  87. package/dist/{owner-C1-csjTo.mjs → owner-CKlRIkeF.mjs} +3 -3
  88. package/dist/{portable-collection-C0MNEEED.mjs → portable-collection-B7RBHVGl.mjs} +2 -2
  89. package/dist/{portable-collection-DkTLVfoM.js → portable-collection-CByyPjLc.js} +2 -2
  90. package/dist/{portable-collection-w6Qn-vJ3.cjs → portable-collection-CmLjeKsP.cjs} +2 -2
  91. package/dist/{portable-key-jtaxB7TD.mjs → portable-key-Clg0IH8C.mjs} +1 -1
  92. package/dist/{proof-CzWCRhnC.mjs → proof-tAb5_Dq4.mjs} +3 -3
  93. package/dist/{send-C9-dwpEH.mjs → send-DzfFREML.mjs} +153 -40
  94. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  95. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  96. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  97. package/dist/sig/compound-proof.test.mjs +2 -2
  98. package/dist/sig/http.test.mjs +38 -2
  99. package/dist/sig/key.test.mjs +1 -1
  100. package/dist/sig/ld.test.mjs +1 -1
  101. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  102. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  103. package/dist/sig/mod.cjs +2 -2
  104. package/dist/sig/mod.d.ts +3 -3
  105. package/dist/sig/mod.js +2 -2
  106. package/dist/sig/owner.test.mjs +1 -1
  107. package/dist/sig/portable-collection.test.mjs +1 -1
  108. package/dist/sig/portable-interop.test.mjs +3 -3
  109. package/dist/sig/portable-key.test.mjs +5 -5
  110. package/dist/sig/proof.test.mjs +3 -3
  111. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  112. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  113. package/dist/sig/verification.test.mjs +3 -3
  114. package/dist/{temporal-lDCl8Zxn.mjs → temporal-D9Nt8Fm4.mjs} +1 -1
  115. package/dist/testing/kv.test.mjs +1 -1
  116. package/dist/testing/mod.d.mts +1030 -412
  117. package/dist/utils/docloader.test.mjs +2 -2
  118. package/dist/utils/kv-cache.test.mjs +1 -1
  119. package/dist/utils/mod.cjs +1 -1
  120. package/dist/utils/mod.d.ts +1 -1
  121. package/dist/utils/mod.js +1 -1
  122. package/package.json +6 -6
  123. package/skills/fedify/SKILL.md +60 -1
@@ -7,15 +7,16 @@ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
8
8
  import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
9
9
  import "../std__assert-DvMDRPes.mjs";
10
+ import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
10
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
11
- import { m as signJsonLd } from "../ld-CmAQYDWE.mjs";
12
- import { s as signObject } from "../proof-CzWCRhnC.mjs";
13
- import { m as verifyRequestDetailed, u as signRequest } from "../http-D5n4S1oL.mjs";
12
+ import { m as signJsonLd } from "../ld-C5JOBBFj.mjs";
13
+ import { s as signObject } from "../proof-tAb5_Dq4.mjs";
14
+ import { m as verifyRequestDetailed, u as signRequest } from "../http-DqOXkYbX.mjs";
14
15
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
15
- import { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-D02eC0j7.mjs";
16
+ import { M as observeOutboundBroadcast, _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-BkRqMGPU.mjs";
16
17
  import { Create, CryptographicKey, Follow, Note, Person, Tombstone } from "@fedify/vocab";
17
18
  import { exportDidKey, formatIri, parseIri } from "@fedify/vocab-runtime";
18
- import { mockDocumentLoader, test } from "@fedify/fixture";
19
+ import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
20
  import { encodeBase64 } from "byte-encodings/base64";
20
21
  //#region src/federation/portable-inbox.test.ts
21
22
  const did = await exportDidKey(ed25519PublicKey.publicKey);
@@ -1531,5 +1532,429 @@ test("onRequestFinished covers portable ingress and recipient lookup failures",
1531
1532
  error: failure
1532
1533
  });
1533
1534
  });
1535
+ function replicationResult(events) {
1536
+ assertEquals(events.map((event) => event.stage), ["started", "preparation-finished"]);
1537
+ assertEquals(events.map((event) => event.operation), ["portable-replication", "portable-replication"]);
1538
+ assertEquals(events[0].broadcastId, events[1].broadcastId);
1539
+ const event = events[1];
1540
+ assert(event.stage === "preparation-finished");
1541
+ return event.result;
1542
+ }
1543
+ test("portable replication observers distinguish disabled, unavailable, and unclaimed work", async (t) => {
1544
+ for (const scenario of [
1545
+ "disabled",
1546
+ "unavailable",
1547
+ "no-targets"
1548
+ ]) await t.step(scenario, async () => {
1549
+ const memory = new MemoryKvStore();
1550
+ const kv = scenario === "unavailable" ? {
1551
+ get: (key) => memory.get(key),
1552
+ set: (key, value, options) => memory.set(key, value, options),
1553
+ delete: (key) => memory.delete(key),
1554
+ list: (prefix) => memory.list(prefix)
1555
+ } : memory;
1556
+ const queue = new RecordingQueue();
1557
+ const { federation } = setup({
1558
+ kv,
1559
+ queue,
1560
+ keyPairs: false,
1561
+ gateways: scenario === "no-targets" ? [LOCAL] : [LOCAL, GATEWAY2],
1562
+ options: { portableInboxForwarding: { maxTargets: scenario === "disabled" ? 0 : 10 } }
1563
+ });
1564
+ const events = [];
1565
+ const deliveries = [];
1566
+ const contexts = [];
1567
+ federation.setOutboundObservers({
1568
+ onDelivery(_ctx, event) {
1569
+ deliveries.push(event);
1570
+ },
1571
+ onBroadcast(ctx, event) {
1572
+ contexts.push(ctx);
1573
+ events.push(event);
1574
+ }
1575
+ });
1576
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
1577
+ assertEquals(replicationResult(events), {
1578
+ type: "skipped",
1579
+ reason: scenario === "disabled" ? "forwarding-disabled" : scenario === "unavailable" ? "forwarding-unavailable" : "no-forwarding-targets"
1580
+ });
1581
+ assertEquals(events[0].activity, {});
1582
+ assertEquals(events[1].activity.type, Follow.typeId.href);
1583
+ assert(events[1].activity.id?.startsWith("ap+ef61://"));
1584
+ assert(Object.isFrozen(events[1].activity));
1585
+ assertStrictEquals(contexts[0], contexts[1]);
1586
+ assertEquals(deliveries, []);
1587
+ assertEquals(queue.outbox.length, 0);
1588
+ });
1589
+ });
1590
+ test("portable replication observers preserve lazy enqueue, unsigned fallback, and failed claims", async (t) => {
1591
+ await t.step("readiness precedes acceptance without delaying earlier handoffs", async () => {
1592
+ const queue = new RecordingQueue();
1593
+ let accept;
1594
+ const acceptance = new Promise((resolve) => {
1595
+ accept = resolve;
1596
+ });
1597
+ queue.enqueue = (message) => {
1598
+ queue.messages.push(message);
1599
+ return queue.messages.length === 1 ? acceptance.then(() => {
1600
+ throw new Error("acceptance unknown");
1601
+ }) : Promise.resolve();
1602
+ };
1603
+ const { federation, reports } = setup({
1604
+ queue,
1605
+ keyPairs: false
1606
+ });
1607
+ const events = [];
1608
+ const deliveries = [];
1609
+ let ready;
1610
+ const prepared = new Promise((resolve) => {
1611
+ ready = resolve;
1612
+ });
1613
+ let release;
1614
+ const observerGate = new Promise((resolve) => {
1615
+ release = resolve;
1616
+ });
1617
+ federation.setOutboundObservers({
1618
+ onDelivery(_ctx, event) {
1619
+ deliveries.push(event);
1620
+ },
1621
+ async onBroadcast(_ctx, event) {
1622
+ events.push(event);
1623
+ if (event.stage === "preparation-finished") {
1624
+ ready();
1625
+ await observerGate;
1626
+ }
1627
+ throw new Error("recorder failed");
1628
+ }
1629
+ });
1630
+ const json = await signedFollow();
1631
+ let settled = false;
1632
+ const pending = federation.fetch(post(inboxUrl(), json), { contextData: void 0 }).finally(() => {
1633
+ settled = true;
1634
+ });
1635
+ await prepared;
1636
+ assertEquals(queue.outbox.length, 1);
1637
+ assertFalse(settled);
1638
+ assertEquals(replicationResult(events), {
1639
+ type: "prepared",
1640
+ preparedFor: "delivery",
1641
+ destinationCount: 2
1642
+ });
1643
+ accept();
1644
+ release();
1645
+ assertEquals((await pending).status, 202);
1646
+ assertEquals(queue.outbox.length, 2);
1647
+ for (const message of queue.outbox) {
1648
+ assertEquals(message.activity, json);
1649
+ assertEquals(message.keys, []);
1650
+ assertFalse("broadcastId" in message);
1651
+ const plan = deliveries.find((event) => event.stage === "planned" && event.destinationInbox === message.inbox);
1652
+ assert(plan);
1653
+ assertEquals(message.outbound, {
1654
+ broadcast: plan.broadcast,
1655
+ deliveryId: plan.deliveryId,
1656
+ destinationInbox: plan.destinationInbox,
1657
+ portableInbox: plan.portableInbox
1658
+ });
1659
+ assertEquals(JSON.parse(JSON.stringify(message.outbound)), message.outbound);
1660
+ assertEquals(structuredClone(message.outbound), message.outbound);
1661
+ }
1662
+ assertEquals(deliveries.filter((event) => event.stage === "planned").length, 2);
1663
+ const handoffs = deliveries.filter((event) => event.stage === "handoff");
1664
+ assertEquals(handoffs.length, 2);
1665
+ assertEquals(handoffs.filter((event) => event.stage === "handoff" && event.result.type === "enqueued").length, 1);
1666
+ assertEquals(handoffs.filter((event) => event.stage === "handoff" && event.result.type === "enqueue-failed" && event.result.acceptance === "unknown").length, 1);
1667
+ for (const event of deliveries) {
1668
+ assertEquals(event.portableInbox, `ap+ef61://${did}/users/alice/inbox`);
1669
+ if (event.stage === "planned") assertEquals(event.mode, "portable-replication");
1670
+ }
1671
+ assertEquals(events.length, 2);
1672
+ const source = events[0].provenance.inboxRequest;
1673
+ assert(source);
1674
+ assertEquals(source.inboxRequestId, reports[0].inboxRequestId);
1675
+ assertEquals(source.requestId, reports[0].requestId);
1676
+ assertEquals(source.context.origin, LOCAL);
1677
+ for (const message of queue.outbox) assertEquals(message.provenance?.inboxRequest, source);
1678
+ for (const event of [...events, ...deliveries]) assertEquals(event.provenance.inboxRequest, source);
1679
+ federation.setOutboundObservers({
1680
+ onDelivery(_ctx, event) {
1681
+ deliveries.push(event);
1682
+ },
1683
+ onBroadcast(_ctx, event) {
1684
+ events.push(event);
1685
+ }
1686
+ });
1687
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 202);
1688
+ assertEquals(replicationResult(events.slice(2)), {
1689
+ type: "skipped",
1690
+ reason: "no-forwarding-targets"
1691
+ });
1692
+ assert(events[0].broadcastId !== events[2].broadcastId);
1693
+ assertEquals(queue.outbox.length, 2);
1694
+ });
1695
+ await t.step("partial claim failure is incomplete planning and preserves ingress acceptance", async () => {
1696
+ const memory = new MemoryKvStore();
1697
+ const cas = memory.cas.bind(memory);
1698
+ const failure = { failed: "second claim" };
1699
+ let claims = 0;
1700
+ memory.cas = (...args) => {
1701
+ if (args[0][1] === "portableInboxForwarding" && ++claims === 2) return Promise.reject(failure);
1702
+ return cas(...args);
1703
+ };
1704
+ const queue = new RecordingQueue();
1705
+ const { federation } = setup({
1706
+ kv: memory,
1707
+ queue,
1708
+ keyPairs: false
1709
+ });
1710
+ const events = [];
1711
+ const deliveries = [];
1712
+ federation.setOutboundObservers({
1713
+ onDelivery(_ctx, event) {
1714
+ deliveries.push(event);
1715
+ },
1716
+ onBroadcast(_ctx, event) {
1717
+ events.push(event);
1718
+ }
1719
+ });
1720
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
1721
+ assertEquals(replicationResult(events), {
1722
+ type: "failed",
1723
+ failure: {
1724
+ kind: "local",
1725
+ phase: "destination-planning",
1726
+ error: failure
1727
+ },
1728
+ planning: "incomplete",
1729
+ plannedDestinationCount: 1
1730
+ });
1731
+ assertEquals(deliveries.map((event) => event.stage), ["planned"]);
1732
+ assertEquals(queue.outbox.length, 0);
1733
+ });
1734
+ await t.step("a handled key failure prepares unsigned queue messages", async () => {
1735
+ const queue = new RecordingQueue();
1736
+ const parameters = forwardingParameters({
1737
+ outboxQueue: queue,
1738
+ getKeys: () => Promise.reject(/* @__PURE__ */ new Error("no keys"))
1739
+ });
1740
+ const ctx = createFederation({ kv: parameters.kv }).createContext(new URL(LOCAL));
1741
+ const events = [];
1742
+ const deliveries = [];
1743
+ await observeOutboundBroadcast(ctx, {
1744
+ onDelivery(_ctx, event) {
1745
+ deliveries.push(event);
1746
+ },
1747
+ onBroadcast: (_ctx, event) => {
1748
+ events.push(event);
1749
+ }
1750
+ }, (observation) => forwardPortableInboxActivity(parameters, observation), "portable-replication");
1751
+ assertEquals(replicationResult(events), {
1752
+ type: "prepared",
1753
+ preparedFor: "delivery",
1754
+ destinationCount: 1
1755
+ });
1756
+ assertEquals(deliveries.map((event) => event.stage), ["planned", "handoff"]);
1757
+ const accepted = deliveries[1];
1758
+ assert(accepted.stage === "handoff" && accepted.result.type === "enqueued");
1759
+ assertEquals(queue.outbox[0].keys, []);
1760
+ assertStrictEquals(queue.outbox[0].activity, parameters.activity);
1761
+ });
1762
+ });
1763
+ test("portable replication attempts settle independently after the forwarding deadline", async () => {
1764
+ const [tracerProvider, exporter] = createTestTracerProvider();
1765
+ let release;
1766
+ const gate = new Promise((resolve) => {
1767
+ release = resolve;
1768
+ });
1769
+ let fetched;
1770
+ const fetching = new Promise((resolve) => {
1771
+ fetched = resolve;
1772
+ });
1773
+ let failed;
1774
+ const failureReported = new Promise((resolve) => {
1775
+ failed = resolve;
1776
+ });
1777
+ let succeeded;
1778
+ const successReported = new Promise((resolve) => {
1779
+ succeeded = resolve;
1780
+ });
1781
+ let ended;
1782
+ const concreteSendsEnded = new Promise((resolve) => {
1783
+ ended = resolve;
1784
+ });
1785
+ const exportSpans = exporter.export.bind(exporter);
1786
+ exporter.export = (spans, callback) => {
1787
+ exportSpans(spans, callback);
1788
+ if (exporter.getSpans("activitypub.send_activity").length === 2) ended();
1789
+ };
1790
+ const attempts = [];
1791
+ const submitted = [];
1792
+ esm_default.spyGlobal().post(`begin:${GATEWAY2}/`, (call) => {
1793
+ submitted.push(JSON.parse(String(call.options.body)));
1794
+ return 503;
1795
+ }).post(`begin:${GATEWAY3}/`, async (call) => {
1796
+ submitted.push(JSON.parse(String(call.options.body)));
1797
+ fetched();
1798
+ await gate;
1799
+ return 202;
1800
+ });
1801
+ try {
1802
+ const { federation, reports } = setup({
1803
+ keyPairs: false,
1804
+ options: {
1805
+ allowPrivateAddress: true,
1806
+ tracerProvider,
1807
+ portableInboxForwarding: { deadline: { milliseconds: 0 } }
1808
+ }
1809
+ });
1810
+ let replacements = 0;
1811
+ federation.setOutboundObservers({
1812
+ senderPayload: true,
1813
+ onAttempt(_ctx, report) {
1814
+ attempts.push(report);
1815
+ if (report.outcome.type === "failed") failed();
1816
+ else succeeded();
1817
+ throw new Error("replication observer failure must be isolated");
1818
+ }
1819
+ });
1820
+ const json = await signedFollow();
1821
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 202);
1822
+ await Promise.all([fetching, failureReported]);
1823
+ assertEquals(attempts.length, 1);
1824
+ assertEquals(attempts[0].outcome.type, "failed");
1825
+ assert(attempts[0].outcome.response.type === "final");
1826
+ assertEquals(attempts[0].outcome.response.metadata.status, 503);
1827
+ assertEquals(attempts[0].httpExchangeCount, 1);
1828
+ federation.setOutboundObservers({ onAttempt() {
1829
+ replacements++;
1830
+ } });
1831
+ release();
1832
+ await Promise.all([successReported, concreteSendsEnded]);
1833
+ assertEquals(attempts.length, 2);
1834
+ assertEquals(attempts[1].outcome.type, "succeeded");
1835
+ assert(attempts[1].outcome.response.type === "final");
1836
+ assertEquals(attempts[1].outcome.response.metadata.status, 202);
1837
+ assertEquals(attempts[1].httpExchangeCount, 1);
1838
+ assertEquals(replacements, 0);
1839
+ assert(attempts[0].attemptId !== attempts[1].attemptId);
1840
+ assert(attempts[0].deliveryId !== attempts[1].deliveryId);
1841
+ assertEquals(attempts[0].broadcast, attempts[1].broadcast);
1842
+ for (const report of attempts) {
1843
+ assertEquals(report.senderPayload.stage, "sender-payload");
1844
+ if (report.senderPayload.type === "unavailable") assertEquals(report.senderPayload.reason, "privacy-filter-unavailable");
1845
+ else {
1846
+ assertEquals(report.senderPayload.representation, "privacy-projection");
1847
+ assert(Object.isFrozen(report.senderPayload.value));
1848
+ }
1849
+ assertEquals(report.destinationInbox, report.inboxUrl);
1850
+ assertEquals(report.portableInbox, `ap+ef61://${did}/users/alice/inbox`);
1851
+ assertEquals(report.provenance.inboxRequest?.inboxRequestId, reports[0].inboxRequestId);
1852
+ assert(report.provenance.inboxRequest);
1853
+ const span = exporter.spans.find((span) => span.spanContext().spanId === report.trace?.spanId);
1854
+ assert(span);
1855
+ assertEquals(span.name, "activitypub.send_activity");
1856
+ }
1857
+ assertEquals(submitted, [json, json]);
1858
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 202);
1859
+ assertEquals(submitted.length, 2);
1860
+ assertEquals(replacements, 0);
1861
+ } finally {
1862
+ release();
1863
+ esm_default.hardReset();
1864
+ await tracerProvider.shutdown();
1865
+ }
1866
+ });
1867
+ test("portable replication observers retain preparation after the forwarding deadline", async (t) => {
1868
+ for (const failed of [false, true]) await t.step(failed ? "late preparation failure" : "late unsigned readiness", async () => {
1869
+ let release;
1870
+ const keyGate = new Promise((resolve) => {
1871
+ release = resolve;
1872
+ });
1873
+ const [tracerProvider, exporter] = createTestTracerProvider();
1874
+ let ended;
1875
+ const concreteSendEnded = new Promise((resolve) => {
1876
+ ended = resolve;
1877
+ });
1878
+ const exportSpans = exporter.export.bind(exporter);
1879
+ exporter.export = (spans, callback) => {
1880
+ exportSpans(spans, callback);
1881
+ if (spans.some((span) => span.name === "activitypub.send_activity")) ended();
1882
+ };
1883
+ const failure = { failed: "late local preparation" };
1884
+ const parameters = forwardingParameters({
1885
+ tracerProvider,
1886
+ options: resolvePortableInboxForwardingOptions({ deadline: { milliseconds: 0 } }),
1887
+ getKeys: async () => {
1888
+ await keyGate;
1889
+ if (failed) Object.defineProperty(parameters.activityId, "href", { get() {
1890
+ throw failure;
1891
+ } });
1892
+ return [];
1893
+ }
1894
+ });
1895
+ const ctx = createFederation({ kv: parameters.kv }).createContext(new URL(LOCAL));
1896
+ const events = [];
1897
+ const deliveries = [];
1898
+ let finished;
1899
+ const preparation = new Promise((resolve) => {
1900
+ finished = resolve;
1901
+ });
1902
+ let fetched;
1903
+ const fetching = new Promise((resolve) => {
1904
+ fetched = resolve;
1905
+ });
1906
+ esm_default.spyGlobal().post(`begin:${GATEWAY2}/`, () => {
1907
+ fetched();
1908
+ return new Response(null, { status: 503 });
1909
+ });
1910
+ try {
1911
+ assertEquals((await observeOutboundBroadcast(ctx, {
1912
+ onDelivery(_ctx, event) {
1913
+ deliveries.push(event);
1914
+ },
1915
+ onBroadcast: (_ctx, event) => {
1916
+ events.push(event);
1917
+ if (event.stage === "preparation-finished") finished();
1918
+ }
1919
+ }, (observation) => forwardPortableInboxActivity(parameters, observation), "portable-replication")).length, 1);
1920
+ assertEquals(events.map((event) => event.stage), ["started"]);
1921
+ release();
1922
+ await preparation;
1923
+ assertEquals(deliveries.map((event) => event.stage), ["planned", failed ? "preparation-failed" : "handoff"]);
1924
+ assertEquals(deliveries[0].deliveryId, deliveries[1].deliveryId);
1925
+ if (failed) {
1926
+ const failedDelivery = deliveries[1];
1927
+ assert(failedDelivery.stage === "preparation-failed");
1928
+ assertStrictEquals(failedDelivery.failure.error, failure);
1929
+ assertEquals(replicationResult(events), {
1930
+ type: "failed",
1931
+ failure: {
1932
+ kind: "local",
1933
+ phase: "request-preparation",
1934
+ error: failure
1935
+ },
1936
+ planning: "complete",
1937
+ plannedDestinationCount: 1
1938
+ });
1939
+ assertEquals(esm_default.callHistory.calls().length, 0);
1940
+ } else {
1941
+ assertEquals(replicationResult(events), {
1942
+ type: "prepared",
1943
+ preparedFor: "delivery",
1944
+ destinationCount: 1
1945
+ });
1946
+ await fetching;
1947
+ await concreteSendEnded;
1948
+ await new Promise((resolve) => setTimeout(resolve, 0));
1949
+ assertEquals(events.length, 2);
1950
+ assertEquals(await forwardPortableInboxActivity(parameters), []);
1951
+ }
1952
+ } finally {
1953
+ release();
1954
+ esm_default.hardReset();
1955
+ await tracerProvider.shutdown();
1956
+ }
1957
+ });
1958
+ });
1534
1959
  //#endregion
1535
1960
  export {};
@@ -7,7 +7,7 @@ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
8
  import { t as assert } from "../assert-D1Eafmd_.mjs";
9
9
  import { r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
10
- import { o as createFederation } from "../middleware-D02eC0j7.mjs";
10
+ import { o as createFederation } from "../middleware-BkRqMGPU.mjs";
11
11
  import { Note, Object as Object$1, Person } from "@fedify/vocab";
12
12
  import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
13
13
  import { test } from "@fedify/fixture";
@@ -6,9 +6,9 @@ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
8
  import { t as assert } from "../assert-D1Eafmd_.mjs";
9
- import { s as signObject } from "../proof-CzWCRhnC.mjs";
9
+ import { s as signObject } from "../proof-tAb5_Dq4.mjs";
10
10
  import { n as ed25519PrivateKey, r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
11
- import { o as createFederation } from "../middleware-D02eC0j7.mjs";
11
+ import { o as createFederation } from "../middleware-BkRqMGPU.mjs";
12
12
  import { Create, Note } from "@fedify/vocab";
13
13
  import { exportDidKey, formatIri, parseIri, toCompatibleEf61Id } from "@fedify/vocab-runtime";
14
14
  import { mockDocumentLoader, test } from "@fedify/fixture";
@@ -8,10 +8,10 @@ import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
9
  import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
10
10
  import { t as assert } from "../assert-D1Eafmd_.mjs";
11
- import { d as verifyPortableObjectProof, s as signObject } from "../proof-CzWCRhnC.mjs";
12
- import { u as signRequest } from "../http-D5n4S1oL.mjs";
11
+ import { d as verifyPortableObjectProof, s as signObject } from "../proof-tAb5_Dq4.mjs";
12
+ import { u as signRequest } from "../http-DqOXkYbX.mjs";
13
13
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
14
- import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-D02eC0j7.mjs";
14
+ import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-BkRqMGPU.mjs";
15
15
  import { configure, reset } from "@logtape/logtape";
16
16
  import { Collection, Create, Note, Object as Object$1, PUBLIC_COLLECTION, Person, Tombstone, lookupObject } from "@fedify/vocab";
17
17
  import { exportDidKey, formatIri, parseIri, toCompatibleEf61Id } from "@fedify/vocab-runtime";
@@ -11,10 +11,10 @@ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
11
11
  import "../std__assert-DvMDRPes.mjs";
12
12
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
13
13
  import { t as assert } from "../assert-D1Eafmd_.mjs";
14
- import { p as verifyRequest } from "../http-D5n4S1oL.mjs";
14
+ import { p as verifyRequest } from "../http-DqOXkYbX.mjs";
15
15
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
16
- import { t as doesActorOwnKey } from "../owner-C1-csjTo.mjs";
17
- import { i as sendActivity, n as extractInboxes, r as fetchWithValidatedRedirects, t as SendActivityError } from "../send-C9-dwpEH.mjs";
16
+ import { t as doesActorOwnKey } from "../owner-CKlRIkeF.mjs";
17
+ import { i as sendActivity, n as extractInboxes, r as fetchWithValidatedRedirects, t as SendActivityError } from "../send-DzfFREML.mjs";
18
18
  import { t as FedifySpanExporter } from "../exporter-DVG68ozH.mjs";
19
19
  import { configure, getLogger, reset, withContext } from "@logtape/logtape";
20
20
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
@@ -1376,5 +1376,46 @@ function recordHops(respond) {
1376
1376
  }
1377
1377
  };
1378
1378
  }
1379
+ test("sendActivity() bounds a stalled diagnostic preview without changing HTTP failure", async () => {
1380
+ const inbox = new URL("https://example.com/lifecycle/stalled-error");
1381
+ let reads = 0;
1382
+ let cancels = 0;
1383
+ let closed = 0;
1384
+ let submissions = 0;
1385
+ const body = new ReadableStream({
1386
+ pull(controller) {
1387
+ if (reads++ === 0) controller.enqueue(new TextEncoder().encode("partial error"));
1388
+ },
1389
+ cancel() {
1390
+ cancels++;
1391
+ return new Promise(() => {});
1392
+ }
1393
+ }, { highWaterMark: 0 });
1394
+ const error = await assertRejects(() => sendActivity({
1395
+ activity: lifecycleActivity,
1396
+ keys: unsignedKeys,
1397
+ inbox
1398
+ }, { openHop: () => Promise.resolve({
1399
+ url: inbox,
1400
+ fetch: () => {
1401
+ submissions++;
1402
+ return Promise.resolve(new Response(body, {
1403
+ status: 503,
1404
+ headers: { "Retry-After": "1" }
1405
+ }));
1406
+ },
1407
+ close: () => {
1408
+ closed++;
1409
+ assertFalse(body.locked);
1410
+ return Promise.resolve();
1411
+ }
1412
+ }) }), SendActivityError);
1413
+ assertEquals(error.statusCode, 503);
1414
+ assertEquals(error.responseBody, "partial error… (timed out)");
1415
+ assertEquals(error.responseHeaders.get("Retry-After"), "1");
1416
+ assertEquals(submissions, 1);
1417
+ assertEquals(cancels, 1);
1418
+ assertEquals(closed, 1);
1419
+ });
1379
1420
  //#endregion
1380
1421
  export {};
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-IG3pHfvZ.mjs";
6
6
  import "../../testing-_9kJHvqh.mjs";
7
- import { o as createFederation } from "../../middleware-D02eC0j7.mjs";
7
+ import { o as createFederation } from "../../middleware-BkRqMGPU.mjs";
8
8
  import { n as ParallelMessageQueue } from "../../mq-eF96QxUT.mjs";
9
9
  import { configure, reset } from "@logtape/logtape";
10
10
  import { delay } from "es-toolkit";
@@ -4,8 +4,8 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-IG3pHfvZ.mjs";
6
6
  import "../../testing-_9kJHvqh.mjs";
7
- import { o as createFederation } from "../../middleware-D02eC0j7.mjs";
8
- import { r as createFederationBuilder } from "../../builder-Da_V_LDj.mjs";
7
+ import { r as createFederationBuilder } from "../../builder-B2H8GsyI.mjs";
8
+ import { o as createFederation } from "../../middleware-BkRqMGPU.mjs";
9
9
  import { t as TaskCodec } from "../../codec-C6rTrpIp.mjs";
10
10
  import { t as InProcessMessageQueue } from "../../mq-eF96QxUT.mjs";
11
11
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
5
5
  import "../std__assert-DvMDRPes.mjs";
6
6
  import { t as assert } from "../assert-D1Eafmd_.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-lDCl8Zxn.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-D9Nt8Fm4.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  //#region src/federation/temporal.test.ts
10
10
  test("hasMalformedKnownTemporalLiteral() detects expanded proof timestamps", async () => {
@@ -6,7 +6,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
- import { o as createFederation, s as handleWebFinger } from "../middleware-D02eC0j7.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-BkRqMGPU.mjs";
10
10
  import { Image, Link, Person, Tombstone } from "@fedify/vocab";
11
11
  import { parseIri } from "@fedify/vocab-runtime";
12
12
  import { createTestMeterProvider, test } from "@fedify/fixture";