@fedify/fedify 2.5.0-dev.2609 → 2.5.0-dev.2618
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-BLCvt0zp.mjs → builder-C-zhu1PC.mjs} +82 -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/{compound-proof-DNhLvscb.mjs → compound-proof-B6Mz3pzt.mjs} +2 -2
- package/dist/{context-ByflxsY9.d.cts → context-C1NqcIct.d.cts} +1061 -443
- package/dist/{context-uQUY5fQT.d.ts → context-xrqq4Hrq.d.ts} +1064 -446
- package/dist/{deno-DYkYmKp0.mjs → deno-CaPDSXDl.mjs} +1 -1
- package/dist/{docloader-B9RO5nh9.mjs → docloader-kYMkiXaB.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +4 -4
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-actorless.test.mjs +5 -5
- package/dist/federation/inbox-report.test.mjs +220 -5
- package/dist/federation/inbox-view.test.mjs +1 -1
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +1 -1
- package/dist/federation/outbound-body.test.d.mts +2 -0
- package/dist/federation/outbound-body.test.mjs +149 -0
- package/dist/federation/outbound-capture.test.d.mts +2 -0
- package/dist/federation/outbound-capture.test.mjs +394 -0
- package/dist/federation/outbound-dispatch.test.d.mts +2 -0
- package/dist/federation/outbound-dispatch.test.mjs +368 -0
- package/dist/federation/outbound-document.test.d.mts +2 -0
- package/dist/federation/outbound-document.test.mjs +171 -0
- package/dist/federation/outbound-provenance.test.d.mts +2 -0
- package/dist/federation/outbound-provenance.test.mjs +543 -0
- package/dist/federation/outbound-report.test.d.mts +2 -0
- package/dist/federation/outbound-report.test.mjs +5475 -0
- package/dist/federation/outbound-workers.test.d.mts +2 -0
- package/dist/federation/outbound-workers.test.mjs +64 -0
- package/dist/federation/outbound-workflow.test.d.mts +2 -0
- package/dist/federation/outbound-workflow.test.mjs +154 -0
- package/dist/federation/outgoing-proof.test.mjs +4 -4
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-delivery.test.mjs +106 -7
- package/dist/federation/portable-gateway-keys.test.mjs +5 -5
- package/dist/federation/portable-inbox.test.mjs +430 -5
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable-route-activity.test.mjs +2 -2
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +44 -3
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{federation-DzMdu4zU.cjs → federation-c-ulWHXG.cjs} +2358 -405
- package/dist/{http-EoFzzh1s.js → http-C12QVNZt.js} +62 -32
- package/dist/{http-BdTJujR0.cjs → http-DwL_g670.cjs} +62 -32
- package/dist/{http-TgeyuTDM.mjs → http-NYqvoigN.mjs} +64 -34
- package/dist/{inbox-ChkbRxCN.mjs → inbox-CkL5YgHZ.mjs} +90 -3
- package/dist/{inbox-view-BosBZdlt.mjs → inbox-view-IJCZG0_b.mjs} +1 -1
- package/dist/{key-DbLNT-vA.d.ts → key-dwwAg5bA.d.ts} +1 -1
- package/dist/{kv-cache-CMgOYiLU.js → kv-cache-1BO1Deou.js} +1 -1
- package/dist/{kv-cache-CTA2i08a.mjs → kv-cache-BdFnnrOU.mjs} +1 -1
- package/dist/{kv-cache-B0o18_Ni.cjs → kv-cache-CcGUsFoZ.cjs} +1 -1
- package/dist/{ld-CZsMAfOw.mjs → ld-CEOS8Pho.mjs} +2 -2
- package/dist/{ld-Inagww4R.d.ts → ld-o4LnhCYY.d.ts} +2 -2
- package/dist/{metrics-Kc9mP1LD.mjs → metrics-CvuJeWaJ.mjs} +1 -1
- package/dist/{metrics-DoRtI0qU.d.ts → metrics-DVKYY6K1.d.ts} +1 -1
- package/dist/{middleware-C1coPz8l.mjs → middleware-BPZ9KhAd.mjs} +1 -1
- package/dist/{middleware-B-pjIvQ8.js → middleware-CzcR7pNr.js} +2303 -351
- package/dist/{middleware-CIRoIVSU.mjs → middleware-DMrpt2wD.mjs} +1573 -325
- package/dist/{mod-B7j30IQf.d.ts → mod-CQC9xlLZ.d.ts} +3 -3
- package/dist/{mod-B4kBw1-8.d.ts → mod-D22ZsiKi.d.ts} +2 -2
- package/dist/{mod-BN0TTfyQ.d.ts → mod-DO0epJGH.d.ts} +1 -1
- package/dist/{mod-DF2gbN_d.d.cts → mod-TwiQgEf3.d.cts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +4 -4
- package/dist/mod.d.ts +8 -8
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/outbound-body-CckPA5gc.mjs +83 -0
- package/dist/outbound-capture-DVgnRzlf.mjs +184 -0
- package/dist/outbound-document-BMgwLXTY.mjs +179 -0
- package/dist/{owner-Dwz4fGUW.mjs → owner-BOyHaAlU.mjs} +3 -3
- package/dist/{portable-collection-BikiijSk.js → portable-collection-B4HFXlgN.js} +2 -2
- package/dist/{portable-collection-Oxfavx8f.mjs → portable-collection-BBSMcaDY.mjs} +2 -2
- package/dist/{portable-collection-Ci6vIhbR.cjs → portable-collection-D_zZTK4C.cjs} +2 -2
- package/dist/{portable-key--fWpTNDN.mjs → portable-key-CF8xK_lj.mjs} +1 -1
- package/dist/{proof-D6Kb0Qq9.mjs → proof-VrE-GaGv.mjs} +3 -3
- package/dist/{send-BFti0L3I.mjs → send-CY6agn9T.mjs} +153 -40
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +2 -2
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +38 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +1 -1
- package/dist/sig/mitra-receive-interop.test.mjs +4 -4
- package/dist/sig/mitra-send-interop.test.mjs +3 -3
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-interop.test.mjs +3 -3
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/sig/tootik-request-interop.test.mjs +4 -4
- package/dist/sig/tootik-send-interop.test.mjs +3 -3
- package/dist/sig/verification.test.mjs +3 -3
- package/dist/{temporal-DlYL_raW.mjs → temporal-QO2pe6W4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +1030 -412
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
- package/skills/fedify/SKILL.md +60 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
6
|
+
import { s as signObject } from "../proof-VrE-GaGv.mjs";
|
|
7
|
+
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
|
|
8
|
+
import { o as createFederation, t as ContextImpl } from "../middleware-DMrpt2wD.mjs";
|
|
9
|
+
import { configure, getLogger, reset, withContext } from "@logtape/logtape";
|
|
10
|
+
import { Create, Note, Person } from "@fedify/vocab";
|
|
11
|
+
import { computeDigestMultibase, exportDidKey, parseIri } from "@fedify/vocab-runtime";
|
|
12
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
13
|
+
import { deepStrictEqual, notStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
+
//#region src/federation/outbound-provenance.test.ts
|
|
16
|
+
const sender = {
|
|
17
|
+
keyId: rsaPublicKey2.id,
|
|
18
|
+
privateKey: rsaPrivateKey2
|
|
19
|
+
};
|
|
20
|
+
const outgoing = new Create({
|
|
21
|
+
id: new URL("https://example.com/outgoing"),
|
|
22
|
+
actor: new URL("https://example.com/person2")
|
|
23
|
+
});
|
|
24
|
+
const target = new Person({
|
|
25
|
+
id: new URL("https://remote.example/bob"),
|
|
26
|
+
inbox: new URL("https://remote.example/inbox")
|
|
27
|
+
});
|
|
28
|
+
const loaders = {
|
|
29
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
30
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
31
|
+
authenticatedDocumentLoaderFactory: () => mockDocumentLoader
|
|
32
|
+
};
|
|
33
|
+
function post(path, body) {
|
|
34
|
+
return new Request(`https://inbox.example:8443${path}`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/activity+json",
|
|
38
|
+
"X-Request-Id": "shared-external-label"
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(body)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function payload() {
|
|
44
|
+
return await new Create({
|
|
45
|
+
id: new URL("https://example.com/incoming"),
|
|
46
|
+
actor: new URL("https://example.com/person2")
|
|
47
|
+
}).toJsonLd({ contextLoader: mockDocumentLoader });
|
|
48
|
+
}
|
|
49
|
+
test("outbound inbox provenance follows clones, conversions, loaders, sends, and forwarding", async (t) => {
|
|
50
|
+
for (const path of ["/users/alice/inbox", "/inbox"]) await t.step(path, async () => {
|
|
51
|
+
const reports = [];
|
|
52
|
+
const events = [];
|
|
53
|
+
const messages = [];
|
|
54
|
+
let retained;
|
|
55
|
+
const federation = createFederation({
|
|
56
|
+
...loaders,
|
|
57
|
+
kv: new MemoryKvStore(),
|
|
58
|
+
origin: "https://canonical.example",
|
|
59
|
+
skipSignatureVerification: true,
|
|
60
|
+
manuallyStartQueue: true,
|
|
61
|
+
queue: { outbox: {
|
|
62
|
+
enqueue(message) {
|
|
63
|
+
messages.push(message);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
},
|
|
66
|
+
listen: () => Promise.resolve()
|
|
67
|
+
} }
|
|
68
|
+
});
|
|
69
|
+
federation.setOutboundObservers({
|
|
70
|
+
onBroadcast(_ctx, event) {
|
|
71
|
+
events.push(event);
|
|
72
|
+
},
|
|
73
|
+
onDelivery(_ctx, event) {
|
|
74
|
+
events.push(event);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
federation.setActorDispatcher("/users/{identifier}", () => new Person({})).setKeyPairsDispatcher(async (ctx) => {
|
|
78
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
79
|
+
return [];
|
|
80
|
+
});
|
|
81
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").setSharedKeyDispatcher(async (ctx) => {
|
|
82
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
83
|
+
return sender;
|
|
84
|
+
}).on(Create, async (ctx) => {
|
|
85
|
+
ok(ctx instanceof ContextImpl);
|
|
86
|
+
const clone = ctx.clone(void 0);
|
|
87
|
+
ok(clone instanceof ContextImpl);
|
|
88
|
+
const outbox = clone.toOutboxContext("alice", {}, outgoing.id.href, Create.typeId.href);
|
|
89
|
+
const inbox = outbox.toInboxContext(null, {}, outgoing.id.href, Create.typeId.href).clone(void 0);
|
|
90
|
+
ctx.url.host = "changed.example:9443";
|
|
91
|
+
retained = inbox;
|
|
92
|
+
await clone.sendActivity(sender, target, outgoing);
|
|
93
|
+
await outbox.clone(void 0).sendActivity(sender, target, outgoing);
|
|
94
|
+
await inbox.forwardActivity(sender, target, { skipIfUnsigned: false });
|
|
95
|
+
}).onRequestFinished(async (ctx, report) => {
|
|
96
|
+
reports.push(report);
|
|
97
|
+
await ctx.clone(void 0).sendActivity(sender, [], outgoing);
|
|
98
|
+
});
|
|
99
|
+
const body = await payload();
|
|
100
|
+
for (let index = 0; index < 2; index++) {
|
|
101
|
+
const start = events.length;
|
|
102
|
+
strictEqual((await federation.fetch(post(path, body), { contextData: void 0 })).status, 202);
|
|
103
|
+
const report = reports[index];
|
|
104
|
+
ok(report.inboxRequestId);
|
|
105
|
+
strictEqual(report.requestId, "shared-external-label");
|
|
106
|
+
const observed = events.slice(start);
|
|
107
|
+
ok(observed.length > 0);
|
|
108
|
+
for (const event of observed) {
|
|
109
|
+
const source = event.provenance.inboxRequest;
|
|
110
|
+
ok(source);
|
|
111
|
+
strictEqual(source.inboxRequestId, report.inboxRequestId);
|
|
112
|
+
strictEqual(source.requestId, report.requestId);
|
|
113
|
+
deepStrictEqual(source.context, {
|
|
114
|
+
origin: "https://inbox.example:8443",
|
|
115
|
+
host: "inbox.example:8443",
|
|
116
|
+
hostname: "inbox.example",
|
|
117
|
+
canonicalOrigin: "https://canonical.example"
|
|
118
|
+
});
|
|
119
|
+
ok(Object.isFrozen(event.provenance));
|
|
120
|
+
ok(Object.isFrozen(source));
|
|
121
|
+
ok(Object.isFrozen(source.context));
|
|
122
|
+
ok(Object.isFrozen(event.local));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
notStrictEqual(reports[0].inboxRequestId, reports[1].inboxRequestId);
|
|
126
|
+
strictEqual(messages.length, 3);
|
|
127
|
+
ok(events.some((event) => event.local.host === "changed.example:9443"));
|
|
128
|
+
ok(events.some((event) => event.local.host === "inbox.example:8443"));
|
|
129
|
+
ok(events.some((event) => "operation" in event && event.operation === "forward"));
|
|
130
|
+
ok(retained);
|
|
131
|
+
const count = events.length;
|
|
132
|
+
await retained.sendActivity(sender, [], outgoing);
|
|
133
|
+
strictEqual(events[count].provenance.inboxRequest?.inboxRequestId, reports[0].inboxRequestId);
|
|
134
|
+
for (const input of [new URL("https://inbox.example:8443"), post(path, body)]) {
|
|
135
|
+
const independent = input instanceof URL ? federation.createContext(input, void 0) : federation.createContext(input, void 0);
|
|
136
|
+
ok(independent instanceof ContextImpl);
|
|
137
|
+
await independent.toInboxContext(null, body, "https://example.com/manual", Create.typeId.href).sendActivity(sender, [], outgoing);
|
|
138
|
+
deepStrictEqual(events.at(-1).provenance, {});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
test("outbound inbox provenance exists before loader preparation fails", async () => {
|
|
143
|
+
const failure = /* @__PURE__ */ new Error("loader construction failed");
|
|
144
|
+
const reports = [];
|
|
145
|
+
const events = [];
|
|
146
|
+
const federation = createFederation({
|
|
147
|
+
...loaders,
|
|
148
|
+
kv: new MemoryKvStore(),
|
|
149
|
+
contextLoaderFactory() {
|
|
150
|
+
throw failure;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
federation.setOutboundObservers({ onBroadcast(_ctx, event) {
|
|
154
|
+
events.push(event);
|
|
155
|
+
} });
|
|
156
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").onRequestFinished(async (ctx, report) => {
|
|
157
|
+
reports.push(report);
|
|
158
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
159
|
+
});
|
|
160
|
+
for (const path of ["/users/alice/inbox", "/inbox"]) {
|
|
161
|
+
await rejects(federation.fetch(post(path, {}), { contextData: void 0 }), (error) => error === failure);
|
|
162
|
+
const report = reports.at(-1);
|
|
163
|
+
ok(report.inboxRequestId);
|
|
164
|
+
strictEqual(report.requestId, "shared-external-label");
|
|
165
|
+
strictEqual(report.outcome.type, "exception");
|
|
166
|
+
strictEqual(events.at(-1)?.provenance.inboxRequest?.inboxRequestId, report.inboxRequestId);
|
|
167
|
+
}
|
|
168
|
+
notStrictEqual(reports[0].inboxRequestId, reports[1].inboxRequestId);
|
|
169
|
+
});
|
|
170
|
+
test("outbound provenance keeps source log labels separate from current request labels", async () => {
|
|
171
|
+
const records = [];
|
|
172
|
+
await reset();
|
|
173
|
+
await configure({
|
|
174
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
175
|
+
loggers: [{
|
|
176
|
+
category: [],
|
|
177
|
+
sinks: ["buffer"],
|
|
178
|
+
lowestLevel: "info"
|
|
179
|
+
}, {
|
|
180
|
+
category: ["logtape", "meta"],
|
|
181
|
+
sinks: []
|
|
182
|
+
}],
|
|
183
|
+
contextLocalStorage: new AsyncLocalStorage()
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
const events = [];
|
|
187
|
+
let retained;
|
|
188
|
+
let report;
|
|
189
|
+
const federation = createFederation({
|
|
190
|
+
...loaders,
|
|
191
|
+
kv: new MemoryKvStore(),
|
|
192
|
+
skipSignatureVerification: true
|
|
193
|
+
});
|
|
194
|
+
federation.setActorDispatcher("/users/{identifier}", async (ctx) => {
|
|
195
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
196
|
+
return new Person({});
|
|
197
|
+
}).setKeyPairsDispatcher(() => []);
|
|
198
|
+
federation.setOutboundObservers({ onBroadcast(_ctx, event) {
|
|
199
|
+
events.push(event);
|
|
200
|
+
getLogger(["test", "provenance"]).info("outbound callback");
|
|
201
|
+
} });
|
|
202
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, async () => {
|
|
203
|
+
await withContext({
|
|
204
|
+
sourceInboxRequestId: "ambient-inbox",
|
|
205
|
+
sourceRequestId: "ambient-source"
|
|
206
|
+
}, () => federation.fetch(new Request("https://inbox.example/users/alice?nested=1", { headers: {
|
|
207
|
+
Accept: "application/activity+json",
|
|
208
|
+
"X-Request-Id": "nested-request"
|
|
209
|
+
} }), { contextData: void 0 }));
|
|
210
|
+
}).onRequestFinished((ctx, value) => {
|
|
211
|
+
retained = ctx;
|
|
212
|
+
report = value;
|
|
213
|
+
getLogger(["test", "provenance"]).info("inbox callback");
|
|
214
|
+
});
|
|
215
|
+
strictEqual((await federation.fetch(post("/users/alice/inbox", await payload()), { contextData: void 0 })).status, 202);
|
|
216
|
+
ok(retained && report?.inboxRequestId);
|
|
217
|
+
const ingress = records.find((record) => record.rawMessage === "inbox callback");
|
|
218
|
+
ok(ingress);
|
|
219
|
+
strictEqual(ingress.properties.inboxRequestId, report.inboxRequestId);
|
|
220
|
+
strictEqual(ingress.properties.requestId, report.requestId);
|
|
221
|
+
const nested = records.find((record) => record.properties.url === "https://inbox.example/users/alice?nested=1" && record.properties.requestId === "nested-request");
|
|
222
|
+
ok(nested);
|
|
223
|
+
strictEqual(nested.properties.inboxRequestId, void 0);
|
|
224
|
+
strictEqual(nested.properties.sourceInboxRequestId, void 0);
|
|
225
|
+
strictEqual(nested.properties.sourceRequestId, void 0);
|
|
226
|
+
const original = records.find((record) => record.properties.url === "https://inbox.example:8443/users/alice/inbox" && record.properties.status === 202);
|
|
227
|
+
ok(original);
|
|
228
|
+
strictEqual(original.properties.inboxRequestId, report.inboxRequestId);
|
|
229
|
+
await withContext({ requestId: "unrelated-current" }, () => retained.sendActivity(sender, [], outgoing));
|
|
230
|
+
const descendant = records.filter((record) => record.rawMessage === "outbound callback").at(-1);
|
|
231
|
+
ok(descendant);
|
|
232
|
+
strictEqual(descendant.properties.requestId, "unrelated-current");
|
|
233
|
+
strictEqual(descendant.properties.sourceRequestId, report.requestId);
|
|
234
|
+
strictEqual(descendant.properties.sourceInboxRequestId, report.inboxRequestId);
|
|
235
|
+
const independent = federation.createContext(new URL("https://inbox.example"), void 0);
|
|
236
|
+
await withContext({
|
|
237
|
+
requestId: "independent-current",
|
|
238
|
+
sourceRequestId: "ambient-source",
|
|
239
|
+
sourceInboxRequestId: "ambient-inbox"
|
|
240
|
+
}, () => independent.sendActivity(sender, [], outgoing));
|
|
241
|
+
deepStrictEqual(events.at(-1).provenance, {});
|
|
242
|
+
const independentLog = records.filter((record) => record.rawMessage === "outbound callback").at(-1);
|
|
243
|
+
ok(independentLog);
|
|
244
|
+
strictEqual(independentLog.properties.requestId, "independent-current");
|
|
245
|
+
strictEqual(independentLog.properties.sourceRequestId, void 0);
|
|
246
|
+
strictEqual(independentLog.properties.sourceInboxRequestId, void 0);
|
|
247
|
+
const start = events.length;
|
|
248
|
+
await federation.fetch(new Request("https://inbox.example/users/alice", { headers: {
|
|
249
|
+
Accept: "application/activity+json",
|
|
250
|
+
"X-Request-Id": "actor-request"
|
|
251
|
+
} }), { contextData: void 0 });
|
|
252
|
+
ok(events.length > start);
|
|
253
|
+
for (const event of events.slice(start)) deepStrictEqual(event.provenance, { originatingRequestId: "actor-request" });
|
|
254
|
+
} finally {
|
|
255
|
+
await reset();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
test("outbound provenance does not invent an inbox cause for programmatic routing", async () => {
|
|
259
|
+
const events = [];
|
|
260
|
+
const federation = createFederation({
|
|
261
|
+
...loaders,
|
|
262
|
+
kv: new MemoryKvStore()
|
|
263
|
+
});
|
|
264
|
+
federation.setOutboundObservers({ onBroadcast(_ctx, event) {
|
|
265
|
+
events.push(event);
|
|
266
|
+
} });
|
|
267
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, async (ctx) => {
|
|
268
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
269
|
+
});
|
|
270
|
+
const ctx = federation.createContext(new URL("https://inbox.example"), void 0);
|
|
271
|
+
const signed = await signObject(new Create({
|
|
272
|
+
id: new URL("https://example.com/programmatic"),
|
|
273
|
+
actor: new URL("https://example.com/person2")
|
|
274
|
+
}), ed25519PrivateKey, ed25519Multikey.id, { contextLoader: mockDocumentLoader });
|
|
275
|
+
strictEqual(await ctx.routeActivity(null, signed), true);
|
|
276
|
+
strictEqual(events.length, 2);
|
|
277
|
+
for (const event of events) deepStrictEqual(event.provenance, {});
|
|
278
|
+
});
|
|
279
|
+
test("outbound provenance retains request labels on gateway dispatchers", async (t) => {
|
|
280
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
281
|
+
const digest = await computeDigestMultibase(new TextEncoder().encode("gateway media"));
|
|
282
|
+
for (const route of [
|
|
283
|
+
"actor",
|
|
284
|
+
"object",
|
|
285
|
+
"collection",
|
|
286
|
+
"media"
|
|
287
|
+
]) await t.step(route, async () => {
|
|
288
|
+
const events = [];
|
|
289
|
+
const federation = createFederation({
|
|
290
|
+
...loaders,
|
|
291
|
+
kv: new MemoryKvStore()
|
|
292
|
+
});
|
|
293
|
+
const send = (ctx) => ctx.clone(void 0).sendActivity(sender, [], outgoing);
|
|
294
|
+
federation.setOutboundObservers({ onBroadcast(_ctx, event) {
|
|
295
|
+
events.push(event);
|
|
296
|
+
} });
|
|
297
|
+
federation.setActorDispatcher("/users/{identifier}", async (ctx) => {
|
|
298
|
+
await send(ctx);
|
|
299
|
+
return route === "collection" ? new Person({
|
|
300
|
+
id: parseIri(`ap+ef61://${did}/users/alice`),
|
|
301
|
+
followers: parseIri(`ap+ef61://${did}/users/alice/followers`)
|
|
302
|
+
}) : null;
|
|
303
|
+
});
|
|
304
|
+
federation.setObjectDispatcher(Note, "/notes/{id}", async (ctx) => {
|
|
305
|
+
await send(ctx);
|
|
306
|
+
return null;
|
|
307
|
+
});
|
|
308
|
+
federation.setFollowersDispatcher("/users/{identifier}/followers", async (ctx) => {
|
|
309
|
+
await send(ctx);
|
|
310
|
+
return { items: [] };
|
|
311
|
+
});
|
|
312
|
+
federation.setHashlinkMediaDispatcher(async (ctx) => {
|
|
313
|
+
await send(ctx);
|
|
314
|
+
return null;
|
|
315
|
+
});
|
|
316
|
+
const suffix = route === "actor" ? `${did}/users/alice` : route === "object" ? `${did}/notes/1` : route === "collection" ? `${did}/users/alice/followers` : `hl:${digest}`;
|
|
317
|
+
strictEqual((await federation.fetch(new Request(`https://example.com/.well-known/apgateway/${suffix}`, { headers: {
|
|
318
|
+
Accept: "application/activity+json",
|
|
319
|
+
"X-Request-Id": `${route}-request`
|
|
320
|
+
} }), { contextData: void 0 })).status, route === "collection" ? 200 : 404);
|
|
321
|
+
strictEqual(events.length, route === "collection" ? 4 : 2);
|
|
322
|
+
for (const event of events) {
|
|
323
|
+
deepStrictEqual(event.provenance, { originatingRequestId: `${route}-request` });
|
|
324
|
+
strictEqual(event.local.host, "example.com");
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
test("queued provenance survives inbox retries, fanout, and outbox worker reconstruction", async (t) => {
|
|
329
|
+
const records = [];
|
|
330
|
+
await reset();
|
|
331
|
+
await configure({
|
|
332
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
333
|
+
loggers: [{
|
|
334
|
+
category: [],
|
|
335
|
+
sinks: ["buffer"],
|
|
336
|
+
lowestLevel: "info"
|
|
337
|
+
}, {
|
|
338
|
+
category: ["logtape", "meta"],
|
|
339
|
+
sinks: []
|
|
340
|
+
}],
|
|
341
|
+
contextLocalStorage: new AsyncLocalStorage()
|
|
342
|
+
});
|
|
343
|
+
try {
|
|
344
|
+
for (const { legacy, native } of [
|
|
345
|
+
{
|
|
346
|
+
legacy: false,
|
|
347
|
+
native: false
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
legacy: false,
|
|
351
|
+
native: true
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
legacy: true,
|
|
355
|
+
native: false
|
|
356
|
+
}
|
|
357
|
+
]) await t.step(`${legacy ? "legacy" : "source"}: ${native ? "native redelivery" : "scheduled retry"}`, async () => {
|
|
358
|
+
const messages = [];
|
|
359
|
+
const events = [];
|
|
360
|
+
const reports = [];
|
|
361
|
+
const queue = {
|
|
362
|
+
nativeRetrial: native,
|
|
363
|
+
enqueue(message) {
|
|
364
|
+
messages.push(message);
|
|
365
|
+
return Promise.resolve();
|
|
366
|
+
},
|
|
367
|
+
listen: () => Promise.resolve()
|
|
368
|
+
};
|
|
369
|
+
const federation = createFederation({
|
|
370
|
+
...loaders,
|
|
371
|
+
kv: new MemoryKvStore(),
|
|
372
|
+
origin: "https://canonical.example",
|
|
373
|
+
skipSignatureVerification: true,
|
|
374
|
+
manuallyStartQueue: true,
|
|
375
|
+
queue: {
|
|
376
|
+
inbox: queue,
|
|
377
|
+
fanout: queue,
|
|
378
|
+
outbox: queue
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
federation.setActorDispatcher("/users/{identifier}", () => new Person({})).setKeyPairsDispatcher(async (ctx) => {
|
|
382
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
383
|
+
return [];
|
|
384
|
+
});
|
|
385
|
+
let calls = 0;
|
|
386
|
+
const failure = /* @__PURE__ */ new Error("retry listener");
|
|
387
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").setSharedKeyDispatcher(async (ctx) => {
|
|
388
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
389
|
+
return sender;
|
|
390
|
+
}).on(Create, async (ctx) => {
|
|
391
|
+
getLogger(["test", "queued-source"]).info("queued listener");
|
|
392
|
+
await ctx.sendActivity(sender, [], outgoing);
|
|
393
|
+
if (++calls === 1) throw failure;
|
|
394
|
+
ok(ctx instanceof ContextImpl);
|
|
395
|
+
const clone = ctx.clone(void 0);
|
|
396
|
+
await clone.sendActivity(sender, target, outgoing, { fanout: "force" });
|
|
397
|
+
await ctx.toOutboxContext("alice", {}, outgoing.id.href, Create.typeId.href).clone(void 0).sendActivity(sender, target, outgoing, { fanout: "skip" });
|
|
398
|
+
await clone.forwardActivity(sender, target, { skipIfUnsigned: false });
|
|
399
|
+
}).onRequestFinished((_ctx, report) => {
|
|
400
|
+
reports.push(report);
|
|
401
|
+
});
|
|
402
|
+
strictEqual((await federation.fetch(post(native ? "/inbox" : "/users/alice/inbox", await payload()), { contextData: void 0 })).status, 202);
|
|
403
|
+
const produced = messages[0];
|
|
404
|
+
ok(produced.type === "inbox");
|
|
405
|
+
ok(produced.provenance?.inboxRequest);
|
|
406
|
+
strictEqual(produced.provenance.inboxRequest.inboxRequestId, reports[0].inboxRequestId);
|
|
407
|
+
deepStrictEqual(JSON.parse(JSON.stringify(produced.provenance)), produced.provenance);
|
|
408
|
+
const task = JSON.parse(JSON.stringify(produced));
|
|
409
|
+
if (legacy) delete task.provenance;
|
|
410
|
+
const expected = task.provenance ?? {};
|
|
411
|
+
federation.setOutboundObservers({
|
|
412
|
+
onBroadcast(_ctx, event) {
|
|
413
|
+
events.push(event);
|
|
414
|
+
},
|
|
415
|
+
onDelivery(_ctx, event) {
|
|
416
|
+
events.push(event);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
const execute = (message) => withContext({
|
|
420
|
+
requestId: "worker-current",
|
|
421
|
+
inboxRequestId: "unrelated-inbox",
|
|
422
|
+
sourceInboxRequestId: "ambient-source",
|
|
423
|
+
sourceRequestId: "ambient-label"
|
|
424
|
+
}, () => federation.processQueuedTask(void 0, message));
|
|
425
|
+
messages.length = 0;
|
|
426
|
+
const logStart = records.length;
|
|
427
|
+
if (native) await rejects(execute(task), (error) => error === failure);
|
|
428
|
+
else await execute(task);
|
|
429
|
+
const retry = native ? task : messages.find((message) => message.type === "inbox");
|
|
430
|
+
ok(retry?.type === "inbox");
|
|
431
|
+
deepStrictEqual(retry.provenance ?? {}, expected);
|
|
432
|
+
strictEqual(retry.attempt, native ? 0 : 1);
|
|
433
|
+
await execute({
|
|
434
|
+
...structuredClone(retry),
|
|
435
|
+
id: crypto.randomUUID()
|
|
436
|
+
});
|
|
437
|
+
strictEqual(calls, 2);
|
|
438
|
+
const fanout = messages.find((message) => message.type === "fanout");
|
|
439
|
+
ok(fanout?.type === "fanout");
|
|
440
|
+
deepStrictEqual(fanout.provenance ?? {}, expected);
|
|
441
|
+
await execute(JSON.parse(JSON.stringify(fanout)));
|
|
442
|
+
const outgoingTasks = messages.filter((message) => message.type === "outbox");
|
|
443
|
+
strictEqual(outgoingTasks.length, 3);
|
|
444
|
+
for (const message of outgoingTasks) {
|
|
445
|
+
deepStrictEqual(message.provenance ?? {}, expected);
|
|
446
|
+
strictEqual("provenance" in message, !legacy);
|
|
447
|
+
}
|
|
448
|
+
const consumer = createFederation({
|
|
449
|
+
kv: new MemoryKvStore(),
|
|
450
|
+
allowPrivateAddress: true,
|
|
451
|
+
origin: "https://consumer.example",
|
|
452
|
+
manuallyStartQueue: true,
|
|
453
|
+
queue: { outbox: queue }
|
|
454
|
+
});
|
|
455
|
+
consumer.setOutboundObservers({ onBroadcast(_ctx, event) {
|
|
456
|
+
events.push(event);
|
|
457
|
+
} });
|
|
458
|
+
let permanent = 0;
|
|
459
|
+
consumer.setOutboxPermanentFailureHandler(async (ctx) => {
|
|
460
|
+
permanent++;
|
|
461
|
+
getLogger(["test", "queued-source"]).info("permanent callback");
|
|
462
|
+
await ctx.clone(void 0).sendActivity(sender, [], outgoing);
|
|
463
|
+
});
|
|
464
|
+
esm_default.mockGlobal().post(target.inboxId.href, 410);
|
|
465
|
+
try {
|
|
466
|
+
await withContext({
|
|
467
|
+
requestId: "worker-current",
|
|
468
|
+
sourceInboxRequestId: "ambient-source",
|
|
469
|
+
sourceRequestId: "ambient-label"
|
|
470
|
+
}, () => consumer.processQueuedTask(void 0, {
|
|
471
|
+
...structuredClone(outgoingTasks[0]),
|
|
472
|
+
baseUrl: "https://worker.example"
|
|
473
|
+
}));
|
|
474
|
+
} finally {
|
|
475
|
+
esm_default.hardReset();
|
|
476
|
+
}
|
|
477
|
+
strictEqual(permanent, 1);
|
|
478
|
+
ok(events.length > 0);
|
|
479
|
+
for (const event of events) {
|
|
480
|
+
deepStrictEqual(event.provenance, expected);
|
|
481
|
+
ok(Object.isFrozen(event.provenance));
|
|
482
|
+
if (!legacy) ok(Object.isFrozen(event.provenance.inboxRequest?.context));
|
|
483
|
+
}
|
|
484
|
+
strictEqual(events.at(-1).local.host, "worker.example");
|
|
485
|
+
strictEqual(events.at(-1).local.canonicalOrigin, "https://consumer.example");
|
|
486
|
+
const workerLogs = records.slice(logStart).filter((record) => record.rawMessage === "queued listener" || record.rawMessage === "permanent callback");
|
|
487
|
+
strictEqual(workerLogs.length, 3);
|
|
488
|
+
for (const record of workerLogs) {
|
|
489
|
+
strictEqual(record.properties.requestId, "worker-current");
|
|
490
|
+
strictEqual(record.properties.inboxRequestId, void 0);
|
|
491
|
+
strictEqual(record.properties.sourceInboxRequestId, expected.inboxRequest?.inboxRequestId);
|
|
492
|
+
strictEqual(record.properties.sourceRequestId, expected.inboxRequest?.requestId);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
} finally {
|
|
496
|
+
await reset();
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
test("queued provenance retains a non-inbox HTTP request label without inventing an inbox cause", async () => {
|
|
500
|
+
const messages = [];
|
|
501
|
+
const queue = {
|
|
502
|
+
enqueue(message) {
|
|
503
|
+
messages.push(message);
|
|
504
|
+
return Promise.resolve();
|
|
505
|
+
},
|
|
506
|
+
listen: () => Promise.resolve()
|
|
507
|
+
};
|
|
508
|
+
const federation = createFederation({
|
|
509
|
+
...loaders,
|
|
510
|
+
kv: new MemoryKvStore(),
|
|
511
|
+
manuallyStartQueue: true,
|
|
512
|
+
queue: {
|
|
513
|
+
fanout: queue,
|
|
514
|
+
outbox: queue
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
federation.setActorDispatcher("/users/{identifier}", async (ctx) => {
|
|
518
|
+
await ctx.sendActivity(sender, target, outgoing, { fanout: "force" });
|
|
519
|
+
return null;
|
|
520
|
+
});
|
|
521
|
+
strictEqual((await federation.fetch(new Request("https://example.com/users/alice", { headers: {
|
|
522
|
+
Accept: "application/activity+json",
|
|
523
|
+
"X-Request-Id": "original-http-label"
|
|
524
|
+
} }), { contextData: void 0 })).status, 404);
|
|
525
|
+
const message = messages[0];
|
|
526
|
+
ok(message?.type === "fanout");
|
|
527
|
+
deepStrictEqual(message.provenance, { originatingRequestId: "original-http-label" });
|
|
528
|
+
const events = [];
|
|
529
|
+
federation.setOutboundObservers({ onDelivery(_ctx, event) {
|
|
530
|
+
events.push(event);
|
|
531
|
+
} });
|
|
532
|
+
await withContext({
|
|
533
|
+
requestId: "worker-current",
|
|
534
|
+
sourceInboxRequestId: "unrelated-inbox"
|
|
535
|
+
}, () => federation.processQueuedTask(void 0, JSON.parse(JSON.stringify(message))));
|
|
536
|
+
strictEqual(events.length, 1);
|
|
537
|
+
deepStrictEqual(events[0].provenance, message.provenance);
|
|
538
|
+
const child = messages[1];
|
|
539
|
+
ok(child?.type === "outbox");
|
|
540
|
+
deepStrictEqual(child.provenance, message.provenance);
|
|
541
|
+
});
|
|
542
|
+
//#endregion
|
|
543
|
+
export {};
|