@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.
- package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
- package/dist/{builder-Da8b9O6v.mjs → builder-DbC8bn6k.mjs} +5 -5
- 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-D1djO5fa.mjs → compound-proof-heeXW2_A.mjs} +2 -2
- package/dist/{context-CQ9H8vNW.d.cts → context-BazwQrHn.d.cts} +304 -4
- package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
- package/dist/{context-BdB54V-A.d.ts → context-GlHrzdSP.d.ts} +304 -4
- package/dist/{deno-eNpfj4oR.mjs → deno-Dpewt_jJ.mjs} +1 -1
- package/dist/{docloader-B-0nqOB1.mjs → docloader-CpsDyqVN.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 +5 -5
- package/dist/federation/handler.test.mjs +21 -6
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-actorless.test.d.mts +2 -0
- package/dist/federation/inbox-actorless.test.mjs +487 -0
- package/dist/federation/inbox-authentication.test.d.mts +2 -0
- package/dist/federation/inbox-authentication.test.mjs +116 -0
- package/dist/federation/inbox-report.test.mjs +5 -5
- package/dist/federation/inbox-view.test.d.mts +2 -0
- package/dist/federation/inbox-view.test.mjs +369 -0
- package/dist/federation/inbox.test.mjs +22 -12
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +9 -9
- package/dist/federation/mod.cjs +2 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +8 -2
- package/dist/federation/mq.test.mjs +36 -1
- 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 +2 -2
- package/dist/federation/portable-gateway-keys.test.mjs +6 -6
- package/dist/federation/portable-inbox.test.mjs +4 -4
- 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 +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +3 -3
- package/dist/federation/tasks/tasks.test.mjs +4 -4
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-BVH7drJA.cjs → federation-D8YexkP1.cjs} +1172 -131
- package/dist/{http-DlNxxW_5.js → http-BBr-VLil.js} +143 -16
- package/dist/{http-BUJ5SFh6.mjs → http-BZkyQIkb.mjs} +4 -4
- package/dist/{http-BfW3wyQ4.cjs → http-CAl7Num5.cjs} +161 -16
- package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
- package/dist/inbox-view-NE9wXdLo.mjs +385 -0
- package/dist/inbox-wDuMKaFW.mjs +312 -0
- package/dist/{kv-cache-D_m3A241.mjs → kv-cache-CTQSfkmp.mjs} +1 -1
- package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-CjgCOz3i.cjs} +1 -1
- package/dist/{kv-cache-7015lbrx.js → kv-cache-U7uz5nx4.js} +1 -1
- package/dist/{ld-CD2CsPxq.mjs → ld-CQFuiah9.mjs} +52 -9
- package/dist/ld-CcuZ4NZa.d.ts +250 -0
- package/dist/ld-Cel1RRZ5.d.cts +250 -0
- package/dist/{metrics-D3LHk6Z4.mjs → metrics-B-QxRSSR.mjs} +1 -1
- package/dist/{middleware-CCDXvXOG.mjs → middleware-BdlNa8BD.mjs} +1 -1
- package/dist/{middleware-CzoWvrdZ.mjs → middleware-CuVa0UuS.mjs} +459 -98
- package/dist/{middleware-N9eM16XE.js → middleware-P-AZEQMn.js} +1169 -141
- package/dist/mod-B2783_3v.d.cts +154 -0
- package/dist/mod-BHQE-7WQ.d.ts +154 -0
- package/dist/{mod-D0bxB8jS.d.ts → mod-DjZfJj9R.d.ts} +1 -1
- package/dist/{mod-B0dGgQsg.d.cts → mod-DvWOv2Ho.d.cts} +1 -1
- package/dist/mod.cjs +5 -4
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/{owner-C5-FVX6t.mjs → owner-ccKkQOAf.mjs} +3 -3
- package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CndnpXfo.mjs} +2 -2
- package/dist/{portable-collection-CC2zNAje.js → portable-collection-DUlQ3s2M.js} +1 -1
- package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-TC-kvkVI.cjs} +1 -1
- package/dist/{portable-key-DNc_flH-.mjs → portable-key-D5tFOoV-.mjs} +1 -1
- package/dist/{proof-Cryb1c70.mjs → proof-DPoF06nz.mjs} +98 -10
- package/dist/{send-AF25pWaJ.mjs → send-CF44O1Df.mjs} +3 -3
- 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 +2 -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.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- 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 +44 -4
- 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-BsAeuUJj.mjs → temporal-XlZ4Wb94.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +262 -2
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
- 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.js +1 -1
- package/package.json +7 -7
- package/dist/inbox-CHvMaIFX.mjs +0 -188
- package/dist/mod-BRpER-B0.d.ts +0 -320
- package/dist/mod-xAfaLS4T.d.cts +0 -320
- package/dist/owner-BF0L9cMt.d.cts +0 -78
- 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,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 {
|
|
6
|
-
import {
|
|
7
|
-
import { l as signRequest } from "../http-
|
|
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-
|
|
10
|
-
import { r as createFederationBuilder } from "../builder-
|
|
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";
|