@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.
- package/dist/{builder-Dndt590h.mjs → builder-BC3eRIbe.mjs} +3 -3
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-lxjaUTAI.d.ts → context-C00pWP8x.d.ts} +6 -0
- package/dist/{context-CnoWxuoB.d.cts → context-ao-qb3eP.d.cts} +6 -0
- package/dist/{deno-D_rTMbAD.mjs → deno-Dv6PIwH_.mjs} +1 -1
- package/dist/{docloader-DpTrZ5sA.mjs → docloader-BgHM-JLc.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +8 -8
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +696 -8
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{http-DGD9d8-A.mjs → http-BWoAqky4.mjs} +2 -2
- package/dist/{http-BS1CmlZ6.js → http-CrEB4I8A.js} +1 -1
- package/dist/{http-DbDnj-Q7.cjs → http-D69BMfAn.cjs} +1 -1
- package/dist/{inbox-BwpBTbhH.mjs → inbox-0Dbv0QVc.mjs} +1 -1
- package/dist/{key-1Gf85RN1.mjs → key-ChKDZfvS.mjs} +1 -1
- package/dist/{kv-cache-BdA7HpfF.cjs → kv-cache-B0JZ30EF.cjs} +1 -1
- package/dist/{kv-cache-BARiicPE.js → kv-cache-CDvE8__9.js} +1 -1
- package/dist/{ld-BuuOR_Ms.mjs → ld-DbSNiv8Q.mjs} +99 -10
- package/dist/{middleware-C5Nypx4Z.mjs → middleware-BA9cM-vz.mjs} +1 -1
- package/dist/{middleware-DDM0j-X6.mjs → middleware-C849kmHB.mjs} +160 -29
- package/dist/{middleware-CQRCnFB4.js → middleware-CEufJR5k.js} +152 -21
- package/dist/{middleware-DThg01d8.cjs → middleware-C_fDk_dx.cjs} +153 -22
- package/dist/{middleware-D9TWYyT_.cjs → middleware-DCtCNota.cjs} +1 -1
- package/dist/{mod-w1oyHseQ.d.cts → mod-6uJWdCc0.d.cts} +1 -1
- package/dist/{mod-DVPS4yam.d.cts → mod-BA6vqC2S.d.cts} +19 -7
- package/dist/{mod-BS-jg3Wx.d.ts → mod-CL9Ep0uo.d.ts} +19 -7
- package/dist/{mod-I-34OMN3.d.ts → mod-DtRh015i.d.ts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-BI33UeIv.mjs → owner-BJeJfxnT.mjs} +2 -2
- package/dist/{proof-GM73LJjx.mjs → proof-CaMBdk_x.mjs} +36 -12
- package/dist/{proof-vVwkNRv4.cjs → proof-DNgQwz9E.cjs} +131 -18
- package/dist/{proof-BBHUW8ev.js → proof-DwooE8kV.js} +132 -19
- package/dist/{send-nImUK91D.mjs → send-CYrAOC1K.mjs} +2 -2
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +2 -2
- package/dist/sig/ld.test.mjs +198 -4
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +71 -2
- package/dist/{temporal-CfNFB7M_.mjs → temporal-BJoGKOXy.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- 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-
|
|
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-
|
|
15
|
-
import { a as compactJsonLd, f as signJsonLd, m as verifyJsonLd, s as detachSignature } from "../ld-
|
|
16
|
-
import { t as doesActorOwnKey } from "../owner-
|
|
17
|
-
import { n as signObject, r as verifyObject } from "../proof-
|
|
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-
|
|
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:
|
|
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 () => {
|