@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2448

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 (117) hide show
  1. package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
  2. package/dist/{builder-Da8b9O6v.mjs → builder-DbC8bn6k.mjs} +5 -5
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{compound-proof-D1djO5fa.mjs → compound-proof-heeXW2_A.mjs} +2 -2
  7. package/dist/{context-CQ9H8vNW.d.cts → context-BazwQrHn.d.cts} +304 -4
  8. package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
  9. package/dist/{context-BdB54V-A.d.ts → context-GlHrzdSP.d.ts} +304 -4
  10. package/dist/{deno-eNpfj4oR.mjs → deno-Dpewt_jJ.mjs} +1 -1
  11. package/dist/{docloader-B-0nqOB1.mjs → docloader-CpsDyqVN.mjs} +2 -2
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/compound-fanout.test.mjs +3 -3
  14. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  15. package/dist/federation/handler.test.mjs +21 -6
  16. package/dist/federation/hashlink-media.test.mjs +2 -2
  17. package/dist/federation/idempotency.test.mjs +2 -2
  18. package/dist/federation/inbox-actorless.test.d.mts +2 -0
  19. package/dist/federation/inbox-actorless.test.mjs +487 -0
  20. package/dist/federation/inbox-authentication.test.d.mts +2 -0
  21. package/dist/federation/inbox-authentication.test.mjs +116 -0
  22. package/dist/federation/inbox-report.test.mjs +5 -5
  23. package/dist/federation/inbox-view.test.d.mts +2 -0
  24. package/dist/federation/inbox-view.test.mjs +369 -0
  25. package/dist/federation/inbox.test.mjs +22 -12
  26. package/dist/federation/metrics.test.mjs +1 -1
  27. package/dist/federation/middleware.test.mjs +9 -9
  28. package/dist/federation/mod.cjs +2 -1
  29. package/dist/federation/mod.d.cts +3 -3
  30. package/dist/federation/mod.d.ts +3 -3
  31. package/dist/federation/mod.js +8 -2
  32. package/dist/federation/mq.test.mjs +36 -1
  33. package/dist/federation/outgoing-proof.test.mjs +4 -4
  34. package/dist/federation/portable-actor.test.mjs +3 -3
  35. package/dist/federation/portable-chain.test.mjs +3 -3
  36. package/dist/federation/portable-collection.test.mjs +4 -4
  37. package/dist/federation/portable-delivery.test.mjs +2 -2
  38. package/dist/federation/portable-gateway-keys.test.mjs +6 -6
  39. package/dist/federation/portable-inbox.test.mjs +4 -4
  40. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  41. package/dist/federation/portable-route-activity.test.mjs +2 -2
  42. package/dist/federation/portable.test.mjs +3 -3
  43. package/dist/federation/send.test.mjs +3 -3
  44. package/dist/federation/tasks/enqueue.test.mjs +3 -3
  45. package/dist/federation/tasks/tasks.test.mjs +4 -4
  46. package/dist/federation/temporal.test.mjs +1 -1
  47. package/dist/federation/webfinger.test.mjs +2 -2
  48. package/dist/{federation-BVH7drJA.cjs → federation-D8YexkP1.cjs} +1172 -131
  49. package/dist/{http-DlNxxW_5.js → http-BBr-VLil.js} +143 -16
  50. package/dist/{http-BUJ5SFh6.mjs → http-BZkyQIkb.mjs} +4 -4
  51. package/dist/{http-BfW3wyQ4.cjs → http-CAl7Num5.cjs} +161 -16
  52. package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
  53. package/dist/inbox-view-NE9wXdLo.mjs +385 -0
  54. package/dist/inbox-wDuMKaFW.mjs +312 -0
  55. package/dist/{kv-cache-D_m3A241.mjs → kv-cache-CTQSfkmp.mjs} +1 -1
  56. package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-CjgCOz3i.cjs} +1 -1
  57. package/dist/{kv-cache-7015lbrx.js → kv-cache-U7uz5nx4.js} +1 -1
  58. package/dist/{ld-CD2CsPxq.mjs → ld-CQFuiah9.mjs} +52 -9
  59. package/dist/ld-CcuZ4NZa.d.ts +250 -0
  60. package/dist/ld-Cel1RRZ5.d.cts +250 -0
  61. package/dist/{metrics-D3LHk6Z4.mjs → metrics-B-QxRSSR.mjs} +1 -1
  62. package/dist/{middleware-CCDXvXOG.mjs → middleware-BdlNa8BD.mjs} +1 -1
  63. package/dist/{middleware-CzoWvrdZ.mjs → middleware-CuVa0UuS.mjs} +459 -98
  64. package/dist/{middleware-N9eM16XE.js → middleware-P-AZEQMn.js} +1169 -141
  65. package/dist/mod-B2783_3v.d.cts +154 -0
  66. package/dist/mod-BHQE-7WQ.d.ts +154 -0
  67. package/dist/{mod-D0bxB8jS.d.ts → mod-DjZfJj9R.d.ts} +1 -1
  68. package/dist/{mod-B0dGgQsg.d.cts → mod-DvWOv2Ho.d.cts} +1 -1
  69. package/dist/mod.cjs +5 -4
  70. package/dist/mod.d.cts +6 -6
  71. package/dist/mod.d.ts +6 -6
  72. package/dist/mod.js +5 -5
  73. package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
  74. package/dist/nodeinfo/handler.test.mjs +2 -2
  75. package/dist/{owner-C5-FVX6t.mjs → owner-ccKkQOAf.mjs} +3 -3
  76. package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CndnpXfo.mjs} +2 -2
  77. package/dist/{portable-collection-CC2zNAje.js → portable-collection-DUlQ3s2M.js} +1 -1
  78. package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-TC-kvkVI.cjs} +1 -1
  79. package/dist/{portable-key-DNc_flH-.mjs → portable-key-D5tFOoV-.mjs} +1 -1
  80. package/dist/{proof-Cryb1c70.mjs → proof-DPoF06nz.mjs} +98 -10
  81. package/dist/{send-AF25pWaJ.mjs → send-CF44O1Df.mjs} +3 -3
  82. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  83. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  84. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  85. package/dist/sig/compound-proof.test.mjs +2 -2
  86. package/dist/sig/http.test.mjs +2 -2
  87. package/dist/sig/key.test.mjs +1 -1
  88. package/dist/sig/ld.test.mjs +1 -1
  89. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  90. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  91. package/dist/sig/mod.cjs +2 -2
  92. package/dist/sig/mod.d.cts +2 -2
  93. package/dist/sig/mod.d.ts +2 -2
  94. package/dist/sig/mod.js +2 -2
  95. package/dist/sig/owner.test.mjs +1 -1
  96. package/dist/sig/portable-collection.test.mjs +1 -1
  97. package/dist/sig/portable-interop.test.mjs +3 -3
  98. package/dist/sig/portable-key.test.mjs +5 -5
  99. package/dist/sig/proof.test.mjs +44 -4
  100. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  101. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  102. package/dist/sig/verification.test.mjs +3 -3
  103. package/dist/{temporal-BsAeuUJj.mjs → temporal-XlZ4Wb94.mjs} +1 -1
  104. package/dist/testing/kv.test.mjs +1 -1
  105. package/dist/testing/mod.d.mts +262 -2
  106. package/dist/testing/mod.mjs +2 -2
  107. package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
  108. package/dist/utils/docloader.test.mjs +2 -2
  109. package/dist/utils/kv-cache.test.mjs +1 -1
  110. package/dist/utils/mod.cjs +1 -1
  111. package/dist/utils/mod.js +1 -1
  112. package/package.json +7 -7
  113. package/dist/inbox-CHvMaIFX.mjs +0 -188
  114. package/dist/mod-BRpER-B0.d.ts +0 -320
  115. package/dist/mod-xAfaLS4T.d.cts +0 -320
  116. package/dist/owner-BF0L9cMt.d.cts +0 -78
  117. package/dist/owner-BSzr5-I8.d.ts +0 -78
@@ -0,0 +1,487 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { a as isActorAuthenticated } from "../inbox-authentication-DflPYlLL.mjs";
5
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
8
+ import "../std__assert-DvMDRPes.mjs";
9
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
10
+ import { m as signJsonLd } from "../ld-CQFuiah9.mjs";
11
+ import { s as signObject } from "../proof-DPoF06nz.mjs";
12
+ import { l as signRequest } from "../http-BZkyQIkb.mjs";
13
+ import { a as rsaPrivateKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
14
+ import { o as createFederation } from "../middleware-CuVa0UuS.mjs";
15
+ import { c as expandDocument } from "../inbox-view-NE9wXdLo.mjs";
16
+ import { Create, FeatureRequest, Person } from "@fedify/vocab";
17
+ import { FetchError, encodeMultibase, preloadedContexts } from "@fedify/vocab-runtime";
18
+ import { mockDocumentLoader, test } from "@fedify/fixture";
19
+ import serialize from "json-canon";
20
+ //#region src/federation/inbox-actorless.test.ts
21
+ const SIGNER = new URL("https://example.com/person2");
22
+ const RSA_KEY = new URL("https://example.com/person2#key3");
23
+ const ED25519_KEY = new URL("https://example.com/person2#key4");
24
+ const TARGET = new URL("https://example.com/users/alice");
25
+ const COLLECTION = new URL("https://example.com/person2/featured/1");
26
+ const MUTABLE_CONTEXT = "https://remote.example/contexts/mutable";
27
+ const RELAY = new URL("https://example.com/relay");
28
+ const RELAY_KEY = new URL("https://example.com/relay#main-key");
29
+ const { publicKeyPem: RELAY_PEM } = await rsaPublicKey2.toJsonLd({ contextLoader: mockDocumentLoader });
30
+ /** The document Mastodon sends, which declares no actor. */
31
+ function featureRequestDocument(extra = {}) {
32
+ return {
33
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/7aa9"],
34
+ id: "https://example.com/person2/featured/1/requests/1",
35
+ type: "FeatureRequest",
36
+ object: TARGET.href,
37
+ instrument: COLLECTION.href,
38
+ ...extra
39
+ };
40
+ }
41
+ var CapturingQueue = class {
42
+ messages = [];
43
+ maxMessageSize;
44
+ constructor(maxMessageSize) {
45
+ this.maxMessageSize = maxMessageSize;
46
+ }
47
+ enqueue(message) {
48
+ this.messages.push(structuredClone(message));
49
+ return Promise.resolve();
50
+ }
51
+ listen() {
52
+ return new Promise(() => {});
53
+ }
54
+ };
55
+ function setup(options = {}) {
56
+ const received = [];
57
+ let mutableLoads = 0;
58
+ const loader = async (url, loaderOptions) => {
59
+ options.contextLoads?.push(url);
60
+ if (options.unavailable?.includes(url)) throw new FetchError(url, "Service unavailable", new Response(null, { status: 503 }));
61
+ if (url === RELAY.href || url === RELAY_KEY.href) return {
62
+ contextUrl: null,
63
+ documentUrl: RELAY.href,
64
+ document: {
65
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
66
+ id: RELAY.href,
67
+ type: "Service",
68
+ publicKey: {
69
+ id: RELAY_KEY.href,
70
+ type: "CryptographicKey",
71
+ owner: RELAY.href,
72
+ publicKeyPem: RELAY_PEM
73
+ }
74
+ }
75
+ };
76
+ if (url === MUTABLE_CONTEXT) {
77
+ mutableLoads++;
78
+ return {
79
+ contextUrl: null,
80
+ documentUrl: url,
81
+ document: { "@context": { tgt: mutableLoads % 2 === 1 ? {
82
+ "@id": "https://www.w3.org/ns/activitystreams#object",
83
+ "@type": "@id"
84
+ } : {
85
+ "@id": "https://www.w3.org/ns/activitystreams#target",
86
+ "@type": "@id"
87
+ } } }
88
+ };
89
+ }
90
+ return await mockDocumentLoader(url, loaderOptions);
91
+ };
92
+ const federation = createFederation({
93
+ kv: new MemoryKvStore(),
94
+ documentLoaderFactory: () => loader,
95
+ contextLoaderFactory: () => loader,
96
+ authenticatedDocumentLoaderFactory: () => loader,
97
+ queue: options.queue,
98
+ manuallyStartQueue: true
99
+ });
100
+ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({
101
+ id: ctx.getActorUri(identifier),
102
+ inbox: ctx.getInboxUri(identifier)
103
+ })).setKeyPairsDispatcher(() => [{
104
+ privateKey: rsaPrivateKey2,
105
+ publicKey: rsaPublicKey2.publicKey
106
+ }]);
107
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(FeatureRequest, (ctx, activity) => {
108
+ if (options.failOnce != null && !options.failOnce.failed) {
109
+ options.failOnce.failed = true;
110
+ throw new Error("Temporary failure.");
111
+ }
112
+ received.push({
113
+ activity,
114
+ authentication: ctx.authentication,
115
+ rawActivity: ctx.activity
116
+ });
117
+ }, { allowActorless: options.allowActorless ?? true }).on(Create, () => {});
118
+ return {
119
+ federation,
120
+ received
121
+ };
122
+ }
123
+ function post(body) {
124
+ return new Request("https://example.com/users/alice/inbox", {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/activity+json" },
127
+ body: JSON.stringify(body)
128
+ });
129
+ }
130
+ async function postSigned(body) {
131
+ return await signRequest(post(body), rsaPrivateKey3, RSA_KEY);
132
+ }
133
+ async function withLds(document) {
134
+ return await signJsonLd(document, rsaPrivateKey3, RSA_KEY, { contextLoader: mockDocumentLoader });
135
+ }
136
+ async function withProof(document) {
137
+ return await (await signObject(await FeatureRequest.fromJsonLd(document, {
138
+ documentLoader: mockDocumentLoader,
139
+ contextLoader: mockDocumentLoader
140
+ }), ed25519PrivateKey, ED25519_KEY, { contextLoader: mockDocumentLoader })).toJsonLd({
141
+ format: "compact",
142
+ contextLoader: mockDocumentLoader
143
+ });
144
+ }
145
+ /**
146
+ * Signs a JSON document as is with an `eddsa-jcs-2022` proof, so that values
147
+ * typed parsing would drop are covered by the proof as well.
148
+ */
149
+ async function withRawProof(document, proofProperty = "proof") {
150
+ const contexts = Array.isArray(document["@context"]) ? document["@context"] : [document["@context"]];
151
+ const unsecured = {
152
+ ...document,
153
+ "@context": [
154
+ ...contexts,
155
+ "https://w3id.org/security/data-integrity/v1",
156
+ ...proofProperty === "proof" ? [] : [{ [proofProperty]: {
157
+ "@id": "https://w3id.org/security#proof",
158
+ "@type": "@id",
159
+ "@container": "@graph"
160
+ } }]
161
+ ]
162
+ };
163
+ const proofConfig = {
164
+ "@context": unsecured["@context"],
165
+ type: "DataIntegrityProof",
166
+ cryptosuite: "eddsa-jcs-2022",
167
+ verificationMethod: ED25519_KEY.href,
168
+ proofPurpose: "assertionMethod",
169
+ created: "2026-10-01T00:00:00Z"
170
+ };
171
+ const digest = async (value) => new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialize(value))));
172
+ const signed = new Uint8Array(await crypto.subtle.sign("Ed25519", ed25519PrivateKey, new Uint8Array([...await digest(proofConfig), ...await digest(unsecured)])));
173
+ const { "@context": _, ...proof } = proofConfig;
174
+ return {
175
+ ...unsecured,
176
+ [proofProperty]: {
177
+ ...proof,
178
+ proofValue: new TextDecoder().decode(encodeMultibase("base58btc", signed))
179
+ }
180
+ };
181
+ }
182
+ async function processQueue(federation, queue) {
183
+ while (queue.messages.length > 0) {
184
+ const message = queue.messages.shift();
185
+ await federation.processQueuedTask(void 0, message);
186
+ }
187
+ }
188
+ test("Actorless FeatureRequest authenticated by HTTP Signatures", async () => {
189
+ const { federation, received } = setup();
190
+ assertEquals((await federation.fetch(await postSigned(featureRequestDocument()), { contextData: void 0 })).status, 202);
191
+ assertEquals(received.length, 1);
192
+ const [{ activity, authentication, rawActivity }] = received;
193
+ assertEquals(activity.actorId, null);
194
+ assertEquals(activity.instrumentId, COLLECTION);
195
+ assertEquals(authentication.verified, true);
196
+ assertEquals(authentication.documentSigners, []);
197
+ assertEquals(authentication.requestSigners.map((p) => [
198
+ p.kind,
199
+ p.id.href,
200
+ p.mechanism
201
+ ]), [[
202
+ "actor",
203
+ SIGNER.href,
204
+ "httpSignatures"
205
+ ]]);
206
+ assert(isActorAuthenticated(authentication, SIGNER));
207
+ assertFalse(isActorAuthenticated(authentication, SIGNER, { scope: "document" }));
208
+ assertEquals(rawActivity, featureRequestDocument());
209
+ });
210
+ test("Actorless FeatureRequest authenticated by Linked Data Signatures", async () => {
211
+ const { federation, received } = setup();
212
+ const document = await withLds(featureRequestDocument());
213
+ assertEquals((await federation.fetch(post(document), { contextData: void 0 })).status, 202);
214
+ assertEquals(received.length, 1);
215
+ assertEquals(received[0].authentication.documentSigners.map((p) => [p.id.href, p.mechanism]), [[SIGNER.href, "linkedDataSignatures"]]);
216
+ assertEquals(received[0].rawActivity, document);
217
+ });
218
+ test("Actorless FeatureRequest authenticated by Object Integrity Proofs", async () => {
219
+ const { federation, received } = setup();
220
+ const document = await withProof(featureRequestDocument());
221
+ assertEquals((await federation.fetch(post(document), { contextData: void 0 })).status, 202);
222
+ assertEquals(received.length, 1);
223
+ assertEquals(received[0].authentication.documentSigners.map((p) => [
224
+ p.kind,
225
+ p.id.href,
226
+ p.mechanism,
227
+ p.keyId?.href
228
+ ]), [[
229
+ "actor",
230
+ SIGNER.href,
231
+ "objectIntegrityProofs",
232
+ ED25519_KEY.href
233
+ ]]);
234
+ });
235
+ test("Actorless activities need a listener that allows them", async () => {
236
+ const { federation, received } = setup({ allowActorless: false });
237
+ const response = await federation.fetch(await postSigned(featureRequestDocument()), { contextData: void 0 });
238
+ assertEquals(response.status, 400);
239
+ assertEquals(await response.text(), "Missing actor.");
240
+ assertEquals(received, []);
241
+ });
242
+ test("Actorless activities need an authenticated principal", async () => {
243
+ const { federation, received } = setup();
244
+ assertEquals((await federation.fetch(post(featureRequestDocument()), { contextData: void 0 })).status, 401);
245
+ assertEquals(received, []);
246
+ });
247
+ test("Actorless activities need the same origin as their signer", async () => {
248
+ const { federation, received } = setup();
249
+ assertEquals((await federation.fetch(await postSigned(featureRequestDocument({ id: "https://other.example/requests/1" })), { contextData: void 0 })).status, 401);
250
+ assertEquals(received, []);
251
+ });
252
+ for (const [label, actor] of [
253
+ ["an anonymous actor", {
254
+ type: "Person",
255
+ name: "Anonymous"
256
+ }],
257
+ ["an untyped actor", { name: "Anonymous" }],
258
+ ["a blank node actor", { id: "_:b0" }]
259
+ ]) for (const mechanism of [
260
+ "http",
261
+ "lds",
262
+ "proof"
263
+ ]) test(`Activities with ${label} are not actorless (${mechanism})`, async () => {
264
+ const queue = new CapturingQueue();
265
+ const { federation, received } = setup({ queue });
266
+ const document = featureRequestDocument({ actor });
267
+ const response = await federation.fetch(mechanism === "http" ? await postSigned(document) : post(mechanism === "lds" ? await withLds(document) : await withRawProof(document)), { contextData: void 0 });
268
+ assert(response.status >= 400, `${response.status}`);
269
+ await processQueue(federation, queue);
270
+ assertEquals(received, []);
271
+ });
272
+ test("Queued actorless activities keep their interpretation and evidence", async () => {
273
+ const queue = new CapturingQueue();
274
+ const contextLoads = [];
275
+ const failOnce = { failed: false };
276
+ const { federation, received } = setup({
277
+ queue,
278
+ contextLoads,
279
+ failOnce
280
+ });
281
+ const document = featureRequestDocument({
282
+ "@context": [
283
+ "https://www.w3.org/ns/activitystreams",
284
+ "https://w3id.org/fep/7aa9",
285
+ MUTABLE_CONTEXT
286
+ ],
287
+ object: void 0,
288
+ tgt: TARGET.href
289
+ });
290
+ delete document.object;
291
+ assertEquals((await federation.fetch(await postSigned(document), { contextData: void 0 })).status, 202);
292
+ assertEquals(queue.messages.length, 1);
293
+ const message = queue.messages[0];
294
+ assertEquals(message.activity, document);
295
+ assertEquals(message.verifiedView?.source, "activity");
296
+ const loadsBefore = contextLoads.filter((url) => url === MUTABLE_CONTEXT).length;
297
+ await processQueue(federation, queue);
298
+ assertEquals(contextLoads.filter((url) => url === MUTABLE_CONTEXT).length, loadsBefore);
299
+ assert(failOnce.failed);
300
+ assertEquals(received.length, 1);
301
+ assertEquals(received[0].activity.objectId, TARGET);
302
+ assertEquals(received[0].activity.targetId, null);
303
+ assertEquals(received[0].authentication.requestSigners.map((p) => p.id.href), [SIGNER.href]);
304
+ assertEquals(received[0].authentication.expandedActivity.length, 1);
305
+ });
306
+ test("Queued actorless activities are dropped without evidence", async () => {
307
+ const queue = new CapturingQueue();
308
+ const { federation, received } = setup({ queue });
309
+ assertEquals((await federation.fetch(await postSigned(featureRequestDocument()), { contextData: void 0 })).status, 202);
310
+ const message = queue.messages.shift();
311
+ const { authentication: _, verifiedView: __, ...legacy } = message;
312
+ await federation.processQueuedTask(void 0, legacy);
313
+ assertEquals(received, []);
314
+ await federation.processQueuedTask(void 0, {
315
+ ...message,
316
+ authentication: {
317
+ verified: true,
318
+ requestSigners: [],
319
+ documentSigners: [],
320
+ originAuthenticated: []
321
+ }
322
+ });
323
+ assertEquals(received, []);
324
+ await federation.processQueuedTask(void 0, {
325
+ ...message,
326
+ verifiedView: { version: 2 }
327
+ });
328
+ assertEquals(received, []);
329
+ await federation.processQueuedTask(void 0, message);
330
+ assertEquals(received.length, 1);
331
+ });
332
+ test("Actorless activities too large to enqueue are processed immediately", async () => {
333
+ const queue = new CapturingQueue(512);
334
+ const { federation, received } = setup({ queue });
335
+ assertEquals((await federation.fetch(await postSigned(featureRequestDocument()), { contextData: void 0 })).status, 202);
336
+ assertEquals(queue.messages, []);
337
+ assertEquals(received.length, 1);
338
+ });
339
+ test("Activities processed by older queue workers are not processed again", async () => {
340
+ const kv = new MemoryKvStore();
341
+ const document = featureRequestDocument({ actor: SIGNER.href });
342
+ await kv.set([
343
+ "_fedify",
344
+ "activityIdempotence",
345
+ "https://example.com",
346
+ document.id
347
+ ], true);
348
+ const federation = createFederation({
349
+ kv,
350
+ documentLoaderFactory: () => mockDocumentLoader,
351
+ contextLoaderFactory: () => mockDocumentLoader,
352
+ authenticatedDocumentLoaderFactory: () => mockDocumentLoader
353
+ });
354
+ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({ id: ctx.getActorUri(identifier) })).setKeyPairsDispatcher(() => [{
355
+ privateKey: rsaPrivateKey2,
356
+ publicKey: rsaPublicKey2.publicKey
357
+ }]);
358
+ let calls = 0;
359
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(FeatureRequest, () => {
360
+ calls++;
361
+ });
362
+ assertEquals((await federation.fetch(await postSigned(document), { contextData: void 0 })).status, 202);
363
+ assertEquals(calls, 0);
364
+ });
365
+ test("Activities with an actor expose their evidence", async () => {
366
+ const { federation, received } = setup();
367
+ assertEquals((await federation.fetch(await postSigned(featureRequestDocument({ actor: SIGNER.href })), { contextData: void 0 })).status, 202);
368
+ assertEquals(received.length, 1);
369
+ assertEquals(received[0].authentication.requestSigners.map((p) => [
370
+ p.id.href,
371
+ p.mechanism,
372
+ p.keyId?.href
373
+ ]), [[
374
+ SIGNER.href,
375
+ "httpSignatures",
376
+ RSA_KEY.href
377
+ ]]);
378
+ });
379
+ test("An author's proof is preferred to a relay's Linked Data Signature", async () => {
380
+ const { federation, received } = setup();
381
+ const relayKey = RELAY_KEY;
382
+ const document = await signJsonLd(await withRawProof(featureRequestDocument()), rsaPrivateKey2, relayKey, { contextLoader: mockDocumentLoader });
383
+ assertEquals((await federation.fetch(post(document), { contextData: void 0 })).status, 202);
384
+ assertEquals(received.length, 1);
385
+ assertEquals(received[0].authentication.documentSigners.map((p) => [p.id.href, p.mechanism]), [[SIGNER.href, "objectIntegrityProofs"]]);
386
+ });
387
+ test("A proof that cannot be verified for now is not superseded", async () => {
388
+ const { federation, received } = setup({ unavailable: ["https://example.com/person2", "https://example.com/person2#key4"] });
389
+ const relayKey = RELAY_KEY;
390
+ const document = await signJsonLd(await withRawProof(featureRequestDocument()), rsaPrivateKey2, relayKey, { contextLoader: mockDocumentLoader });
391
+ let status;
392
+ try {
393
+ status = (await federation.fetch(post(document), { contextData: void 0 })).status;
394
+ } catch {}
395
+ assert(status == null || status >= 500, `${status}`);
396
+ assertEquals(received, []);
397
+ });
398
+ test("Context.routeActivity() exposes the evidence of verified proofs", async () => {
399
+ const { federation, received } = setup();
400
+ const document = await withRawProof(featureRequestDocument({ actor: SIGNER.href }));
401
+ const activity = await FeatureRequest.fromJsonLd(document, {
402
+ documentLoader: mockDocumentLoader,
403
+ contextLoader: mockDocumentLoader
404
+ });
405
+ assert(await federation.createContext(new URL("https://example.com/"), void 0).routeActivity("alice", activity));
406
+ assertEquals(received.length, 1);
407
+ assertEquals(received[0].authentication.documentSigners.map((p) => [p.id.href, p.mechanism]), [[SIGNER.href, "objectIntegrityProofs"]]);
408
+ assertEquals(received[0].authentication.originAuthenticated, []);
409
+ });
410
+ test("An author's aliased proof is preferred to a relay's signature", async () => {
411
+ const { federation, received } = setup();
412
+ const document = await signJsonLd(await withRawProof(featureRequestDocument(), "integrity"), rsaPrivateKey2, RELAY_KEY, { contextLoader: mockDocumentLoader });
413
+ assertEquals((await federation.fetch(post(document), { contextData: void 0 })).status, 202);
414
+ assertEquals(received.length, 1);
415
+ assertEquals(received[0].authentication.documentSigners.map((p) => [p.id.href, p.mechanism]), [[SIGNER.href, "objectIntegrityProofs"]]);
416
+ });
417
+ for (const queued of [false, true]) test(`Context.routeActivity() keeps the fetched interpretation (queued: ${queued})`, async () => {
418
+ const queue = queued ? new CapturingQueue() : void 0;
419
+ const { federation, received } = setup({ queue });
420
+ const activityId = "https://example.com/person2/activities/fetched";
421
+ const fetchedDocument = {
422
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/7aa9"],
423
+ id: activityId,
424
+ type: "FeatureRequest",
425
+ actor: SIGNER.href,
426
+ object: TARGET.href,
427
+ instrument: COLLECTION.href
428
+ };
429
+ const overriding = async (url, options) => {
430
+ if (url === "https://www.w3.org/ns/activitystreams") return {
431
+ contextUrl: null,
432
+ documentUrl: url,
433
+ document: { "@context": {
434
+ ...preloadedContexts[url]["@context"],
435
+ object: {
436
+ "@id": "https://www.w3.org/ns/activitystreams#target",
437
+ "@type": "@id"
438
+ }
439
+ } }
440
+ };
441
+ return await mockDocumentLoader(url, options);
442
+ };
443
+ const context = federation.createContext(new URL("https://example.com/"), void 0);
444
+ const activity = await FeatureRequest.fromJsonLd(fetchedDocument, {
445
+ documentLoader: mockDocumentLoader,
446
+ contextLoader: mockDocumentLoader
447
+ });
448
+ assert(await context.routeActivity("alice", activity, {
449
+ contextLoader: overriding,
450
+ documentLoader: async (url, options) => url === activityId ? {
451
+ contextUrl: null,
452
+ documentUrl: url,
453
+ document: structuredClone(fetchedDocument)
454
+ } : await mockDocumentLoader(url, options)
455
+ }));
456
+ if (queue != null) await processQueue(federation, queue);
457
+ assertEquals(received.length, 1);
458
+ const [{ activity: dispatched, authentication }] = received;
459
+ const root = authentication.expandedActivity[0];
460
+ assertEquals(root["https://www.w3.org/ns/activitystreams#object"]?.[0]?.["@id"], dispatched.objectId?.href);
461
+ assertEquals(dispatched.objectId, TARGET);
462
+ assertEquals(authentication.originAuthenticated.map((p) => p.id.href), [SIGNER.href]);
463
+ });
464
+ test("Activities whose contexts cannot be kept are processed immediately", async () => {
465
+ const queue = new CapturingQueue();
466
+ const { federation, received } = setup({ queue });
467
+ const cached = "https://remote.example/contexts/static-shared";
468
+ await expandDocument({
469
+ "@context": cached,
470
+ "z": "z"
471
+ }, (url, options) => url === cached ? Promise.resolve({
472
+ contextUrl: null,
473
+ documentUrl: url,
474
+ document: { "@context": { z: "https://example.com/z" } },
475
+ tag: "static"
476
+ }) : mockDocumentLoader(url, options));
477
+ const document = featureRequestDocument({ "@context": [
478
+ "https://www.w3.org/ns/activitystreams",
479
+ "https://w3id.org/fep/7aa9",
480
+ cached
481
+ ] });
482
+ assertEquals((await federation.fetch(await postSigned(document), { contextData: void 0 })).status, 202);
483
+ assertEquals(queue.messages, []);
484
+ assertEquals(received.length, 1);
485
+ });
486
+ //#endregion
487
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";
@@ -0,0 +1,116 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { a as isActorAuthenticated, i as getEmptyAuthenticationEvidence, n as deserializeAuthentication, o as serializeAuthentication, t as createAuthenticationEvidence } from "../inbox-authentication-DflPYlLL.mjs";
5
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
7
+ import "../std__assert-DvMDRPes.mjs";
8
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
9
+ import { parseIri } from "@fedify/vocab-runtime";
10
+ import { test } from "@fedify/fixture";
11
+ //#region src/federation/inbox-authentication.test.ts
12
+ const DID = "did:key:z6MkhVPuyvgG1RkMv67azDqDCDERPXVrUg1i3qchXY5EACE3";
13
+ const OTHER_DID = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
14
+ function evidence(overrides) {
15
+ return createAuthenticationEvidence({
16
+ ...getEmptyAuthenticationEvidence(),
17
+ verified: true,
18
+ ...overrides
19
+ });
20
+ }
21
+ function actor(id, mechanism = "httpSignatures") {
22
+ return {
23
+ kind: "actor",
24
+ id: parseIri(id),
25
+ mechanism,
26
+ keyId: null
27
+ };
28
+ }
29
+ test("isActorAuthenticated() uses the effective principals", () => {
30
+ const alice = new URL("https://example.com/users/alice");
31
+ const relay = new URL("https://relay.example/actor");
32
+ const forwarded = evidence({
33
+ requestSigners: [actor(relay.href)],
34
+ documentSigners: [actor(alice.href, "linkedDataSignatures")]
35
+ });
36
+ assert(isActorAuthenticated(forwarded, alice));
37
+ assertFalse(isActorAuthenticated(forwarded, relay));
38
+ const delivered = evidence({ requestSigners: [actor(alice.href)] });
39
+ assert(isActorAuthenticated(delivered, alice));
40
+ assertFalse(isActorAuthenticated(delivered, alice, { scope: "document" }));
41
+ assert(isActorAuthenticated(evidence({ originAuthenticated: [actor(alice.href, "origin")] }), alice));
42
+ assertFalse(isActorAuthenticated(getEmptyAuthenticationEvidence(), alice));
43
+ assertFalse(isActorAuthenticated(delivered, new URL("https://example.com/users/alice/")));
44
+ });
45
+ test("isActorAuthenticated() compares portable actors canonically", () => {
46
+ const portable = parseIri(`ap://${DID}/actor`);
47
+ const compatible = new URL(`https://gateway.example/.well-known/apgateway/${DID}/actor`);
48
+ const otherGateway = new URL(`https://other.example/.well-known/apgateway/${DID}/actor`);
49
+ const byActor = evidence({ requestSigners: [actor(portable.href)] });
50
+ assert(isActorAuthenticated(byActor, compatible));
51
+ assert(isActorAuthenticated(byActor, otherGateway));
52
+ assertFalse(isActorAuthenticated(byActor, parseIri(`ap://${DID}/other`)));
53
+ const byAuthority = evidence({ documentSigners: [{
54
+ kind: "portableAuthority",
55
+ id: new URL(DID),
56
+ mechanism: "objectIntegrityProofs",
57
+ keyId: new URL(`${DID}#${DID.slice(8)}`)
58
+ }] });
59
+ assert(isActorAuthenticated(byAuthority, portable));
60
+ assert(isActorAuthenticated(byAuthority, compatible));
61
+ assertFalse(isActorAuthenticated(byAuthority, parseIri(`ap://${OTHER_DID}/actor`)));
62
+ assertFalse(isActorAuthenticated(byAuthority, new URL("https://example.com/users/a")));
63
+ assertFalse(isActorAuthenticated(byActor, new URL("https://gateway.example/.well-known/apgateway/did:bogus")));
64
+ });
65
+ test("serializeAuthentication() round-trips through JSON", () => {
66
+ const original = evidence({
67
+ requestSigners: [{
68
+ kind: "actor",
69
+ id: new URL("https://example.com/users/alice"),
70
+ mechanism: "httpSignatures",
71
+ keyId: new URL("https://example.com/users/alice#main-key")
72
+ }],
73
+ documentSigners: [{
74
+ kind: "portableAuthority",
75
+ id: new URL(DID),
76
+ mechanism: "objectIntegrityProofs",
77
+ keyId: null
78
+ }]
79
+ });
80
+ const expanded = [{ "@id": "https://example.com/activities/1" }];
81
+ assertEquals(deserializeAuthentication(JSON.parse(JSON.stringify(serializeAuthentication(original))), expanded, null), {
82
+ ...original,
83
+ expandedActivity: expanded
84
+ });
85
+ for (const malformed of [
86
+ null,
87
+ {},
88
+ {
89
+ ...serializeAuthentication(original),
90
+ verified: "yes"
91
+ },
92
+ {
93
+ ...serializeAuthentication(original),
94
+ documentSigners: null
95
+ },
96
+ {
97
+ ...serializeAuthentication(original),
98
+ requestSigners: [{
99
+ kind: "server",
100
+ id: "x",
101
+ mechanism: "origin"
102
+ }]
103
+ },
104
+ {
105
+ ...serializeAuthentication(original),
106
+ requestSigners: [{
107
+ kind: "actor",
108
+ id: "not a url",
109
+ mechanism: "httpSignatures",
110
+ keyId: null
111
+ }]
112
+ }
113
+ ]) assertEquals(deserializeAuthentication(malformed, [], null), null);
114
+ });
115
+ //#endregion
116
+ export {};
@@ -2,12 +2,12 @@ import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
- import { p as signJsonLd } from "../ld-CD2CsPxq.mjs";
6
- import { o as signObject } from "../proof-Cryb1c70.mjs";
7
- import { l as signRequest } from "../http-BUJ5SFh6.mjs";
5
+ import { m as signJsonLd } from "../ld-CQFuiah9.mjs";
6
+ import { s as signObject } from "../proof-DPoF06nz.mjs";
7
+ import { l as signRequest } from "../http-BZkyQIkb.mjs";
8
8
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
9
- import { o as createFederation } from "../middleware-CzoWvrdZ.mjs";
10
- import { r as createFederationBuilder } from "../builder-Da8b9O6v.mjs";
9
+ import { o as createFederation } from "../middleware-CuVa0UuS.mjs";
10
+ import { r as createFederationBuilder } from "../builder-DbC8bn6k.mjs";
11
11
  import { Create, Person } from "@fedify/vocab";
12
12
  import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
13
13
  import { mockDocumentLoader, test } from "@fedify/fixture";
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";