@fedify/fedify 2.4.0-dev.2084 → 2.4.0-dev.2093
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-BGlpUocK.mjs → builder-Ld4Y9ymR.mjs} +34 -3
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-B6Aj2kGY.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
- package/dist/{deno-Dn-py9Zy.mjs → deno-8t8k_6Yv.mjs} +1 -1
- package/dist/{docloader-BUq5bYJr.mjs → docloader-DJKcsAiF.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 +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- 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.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +254 -5
- package/dist/federation/portable-gateway-keys.test.mjs +72 -11
- package/dist/federation/portable-inbox.test.mjs +116 -4
- package/dist/federation/portable.test.mjs +182 -4
- package/dist/federation/send.test.mjs +3 -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 +89 -1
- package/dist/{federation-BLGx8JUH.cjs → federation-aSHO04ob.cjs} +360 -65
- package/dist/{http-B0VRNipE.mjs → http-B2ftY0Ha.mjs} +4 -4
- package/dist/{http-B5q1CHtL.cjs → http-DSaSAN7X.cjs} +1609 -1457
- package/dist/{http-4PCCvhsU.js → http-DyT7DP1Y.js} +1518 -1390
- package/dist/{key-BJrp524f.mjs → key-Dwm4VswT.mjs} +39 -99
- package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
- package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-D0yYukqk.mjs} +1 -1
- package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DmroAz3M.js} +1 -1
- package/dist/{ld-BmFz5NLA.mjs → ld-BYZOoDzZ.mjs} +3 -3
- package/dist/{metrics-Beod6tzO.mjs → metrics-B7YwKTFr.mjs} +1 -1
- package/dist/{middleware-B7CNsyqt.mjs → middleware-BNeqKSsL.mjs} +1 -1
- package/dist/{middleware-D_9pNYe9.js → middleware-Blq0882d.js} +368 -73
- package/dist/{middleware-BKeQYPm0.mjs → middleware-E_0IH72m.mjs} +166 -71
- package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
- package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-CsKVgMzk.mjs → owner-Cgx2Dc7I.mjs} +5 -4
- package/dist/{owner-_pbbgLJK.js → owner-DAGSC5gX.js} +2 -2
- package/dist/{owner-BQFZtFV4.cjs → owner-szIZl4qI.cjs} +2 -2
- package/dist/{portable-key-B4R-CiIV.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-ozG4HfcQ.mjs → proof-DVXC6nYz.mjs} +59 -18
- package/dist/{send-ClPt8nYu.mjs → send-D-hOvYKy.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 +160 -3
- 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 +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +69 -5
- package/dist/sig/portable-key.test.mjs +176 -10
- package/dist/sig/proof.test.mjs +107 -4
- package/dist/{sig-DhdomAN0.js → sig-B5Ell7pO.js} +19 -3
- package/dist/{sig-k3C8sF7g.cjs → sig-CXOKN4Br.cjs} +19 -3
- package/dist/{temporal-Ba0EAZAI.mjs → temporal-CqpKGEPk.mjs} +1 -1
- package/dist/testing/kv.test.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
|
@@ -7,12 +7,13 @@ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
|
7
7
|
import "../std__assert-DvMDRPes.mjs";
|
|
8
8
|
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
9
9
|
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
10
|
-
import { t as exportJwk } from "../key-
|
|
11
|
-
import { s as detachSignature } from "../ld-
|
|
12
|
-
import {
|
|
10
|
+
import { t as exportJwk } from "../key-Dwm4VswT.mjs";
|
|
11
|
+
import { s as detachSignature } from "../ld-BYZOoDzZ.mjs";
|
|
12
|
+
import { i as signObject } from "../proof-DVXC6nYz.mjs";
|
|
13
13
|
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
14
|
-
import { n as FederationImpl, o as createFederation } from "../middleware-
|
|
15
|
-
import { a as verifyCompoundPortableObjectProofs } from "../compound-proof-
|
|
14
|
+
import { c as assertPortableActorActivity, n as FederationImpl, o as createFederation } from "../middleware-E_0IH72m.mjs";
|
|
15
|
+
import { a as verifyCompoundPortableObjectProofs } from "../compound-proof-CTx_lewQ.mjs";
|
|
16
|
+
import { configure, reset } from "@logtape/logtape";
|
|
16
17
|
import { Create, Note, Person } from "@fedify/vocab";
|
|
17
18
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
18
19
|
import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
|
|
@@ -222,6 +223,29 @@ test("a non-portable activity embedding portable objects needs one key", async (
|
|
|
222
223
|
assertEquals(accepted.bodies.length, 1);
|
|
223
224
|
assertEquals(proofOf(accepted.bodies[0]).verificationMethod, ed25519PublicKey.id.href);
|
|
224
225
|
});
|
|
226
|
+
test("a non-portable activity embedding compatible-ID objects needs one key", async () => {
|
|
227
|
+
const owner = await didKey();
|
|
228
|
+
const compatibleId = (path) => new URL(`https://gw.example/.well-known/apgateway/${owner.did}${path}`);
|
|
229
|
+
const child = await signObject(new Note({
|
|
230
|
+
id: compatibleId(`/objects/${crypto.randomUUID()}`),
|
|
231
|
+
attribution: compatibleId("/actor"),
|
|
232
|
+
content: "A portable note"
|
|
233
|
+
}), owner.privateKey, owner.keyId, {
|
|
234
|
+
...options,
|
|
235
|
+
context: childContext,
|
|
236
|
+
created
|
|
237
|
+
});
|
|
238
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
239
|
+
const keys = [sender(await didKey()), sender(await didKey())];
|
|
240
|
+
assertEquals((await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, httpCreate(child)), TypeError, "exactly one Ed25519 key"))).bodies.length, 0);
|
|
241
|
+
const key = {
|
|
242
|
+
keyId: ed25519PublicKey.id,
|
|
243
|
+
privateKey: ed25519PrivateKey
|
|
244
|
+
};
|
|
245
|
+
const accepted = await capture(() => ctx.sendActivity([rsaKey, key], recipient, httpCreate(child)));
|
|
246
|
+
assertEquals(accepted.bodies.length, 1);
|
|
247
|
+
await assertCompoundVerifies(accepted.bodies[0]);
|
|
248
|
+
});
|
|
225
249
|
test("an activity without portable objects is still signed by every key", async () => {
|
|
226
250
|
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
227
251
|
const keys = [await didKey(), await didKey()];
|
|
@@ -392,5 +416,230 @@ test("a Fedify inbox accepts a portable activity Fedify produced", async () => {
|
|
|
392
416
|
});
|
|
393
417
|
assertEquals([(await receiving.fetch(requests[0], { contextData: void 0 })).status, received], [202, 1]);
|
|
394
418
|
});
|
|
419
|
+
const gateway = "https://gw.example";
|
|
420
|
+
function compatibleId(did, path, origin = gateway) {
|
|
421
|
+
return new URL(`${origin}/.well-known/apgateway/${did}${path}`);
|
|
422
|
+
}
|
|
423
|
+
function compatibleCreate(owner, object, { id, actor } = {}) {
|
|
424
|
+
return new Create({
|
|
425
|
+
id: id ?? compatibleId(owner.did, `/activities/${crypto.randomUUID()}`),
|
|
426
|
+
actor: actor ?? compatibleId(owner.did, "/actor"),
|
|
427
|
+
object
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
test("a compatible-ID activity is signed only by the key matching its DID", async () => {
|
|
431
|
+
const owner = await didKey();
|
|
432
|
+
const childOwner = await didKey();
|
|
433
|
+
const child = await signedChild(childOwner);
|
|
434
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
435
|
+
const { bodies, requests } = await capture(async () => ctx.sendActivity([
|
|
436
|
+
rsaKey,
|
|
437
|
+
sender(await didKey()),
|
|
438
|
+
sender(owner),
|
|
439
|
+
sender(childOwner)
|
|
440
|
+
], recipient, compatibleCreate(owner, child)));
|
|
441
|
+
assertEquals(bodies.length, 1);
|
|
442
|
+
assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
|
|
443
|
+
assert(requests[0].headers.has("Signature"));
|
|
444
|
+
assertEquals(bodies[0].signature, void 0);
|
|
445
|
+
await assertCompoundVerifies(bodies[0]);
|
|
446
|
+
});
|
|
447
|
+
test("a compatible-ID actor's activity needs an ID of the actor's DID", async () => {
|
|
448
|
+
const owner = await didKey();
|
|
449
|
+
const other = await didKey();
|
|
450
|
+
const cases = [
|
|
451
|
+
[
|
|
452
|
+
"a compatible activity ID of another DID",
|
|
453
|
+
compatibleCreate(owner, void 0, { id: compatibleId(other.did, "/activities/1") }),
|
|
454
|
+
"with the same DID"
|
|
455
|
+
],
|
|
456
|
+
[
|
|
457
|
+
"an ap: activity ID of another DID",
|
|
458
|
+
compatibleCreate(owner, void 0, { id: parseIri(`ap://${other.did}/activities/1`) }),
|
|
459
|
+
"with the same DID"
|
|
460
|
+
],
|
|
461
|
+
[
|
|
462
|
+
"an ordinary activity ID",
|
|
463
|
+
compatibleCreate(owner, void 0, { id: new URL("https://gw.example/activities/1") }),
|
|
464
|
+
"with the same DID"
|
|
465
|
+
],
|
|
466
|
+
[
|
|
467
|
+
"a malformed compatible activity ID",
|
|
468
|
+
compatibleCreate(owner, void 0, { id: new URL(compatibleId(owner.did, "/activities/1").href + "?@gateway=https%3A%2F%2Fgw.example") }),
|
|
469
|
+
"with the same DID"
|
|
470
|
+
],
|
|
471
|
+
[
|
|
472
|
+
"a malformed compatible actor ID",
|
|
473
|
+
compatibleCreate(owner, void 0, { actor: new URL(compatibleId(owner.did, "/actor").href + "?@gateway=https%3A%2F%2Fgw.example") }),
|
|
474
|
+
"malformed FEP-ef61 portable ID"
|
|
475
|
+
],
|
|
476
|
+
[
|
|
477
|
+
"compatible actors of different DIDs",
|
|
478
|
+
new Create({
|
|
479
|
+
id: compatibleId(owner.did, "/activities/1"),
|
|
480
|
+
actors: [compatibleId(owner.did, "/actor"), compatibleId(other.did, "/actor")]
|
|
481
|
+
}),
|
|
482
|
+
"different DIDs"
|
|
483
|
+
]
|
|
484
|
+
];
|
|
485
|
+
for (const [name, activity, message] of cases) {
|
|
486
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
487
|
+
const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, activity), TypeError, message));
|
|
488
|
+
assertEquals(bodies.length, 0, name);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
test("a compatible-ID activity without exactly one DID-matching key is rejected", async () => {
|
|
492
|
+
const owner = await didKey();
|
|
493
|
+
const cases = [
|
|
494
|
+
[
|
|
495
|
+
"no key matches",
|
|
496
|
+
[sender(await didKey())],
|
|
497
|
+
"none of its 1 Ed25519 keys"
|
|
498
|
+
],
|
|
499
|
+
[
|
|
500
|
+
"only a gateway key",
|
|
501
|
+
[{
|
|
502
|
+
keyId: compatibleId(owner.did, "/actor#main-key"),
|
|
503
|
+
privateKey: owner.privateKey
|
|
504
|
+
}],
|
|
505
|
+
"none of its 0 Ed25519 keys"
|
|
506
|
+
],
|
|
507
|
+
[
|
|
508
|
+
"several keys match",
|
|
509
|
+
[sender(owner), {
|
|
510
|
+
keyId: new URL(`${owner.did}#second`),
|
|
511
|
+
privateKey: owner.privateKey
|
|
512
|
+
}],
|
|
513
|
+
"2 of its Ed25519 keys"
|
|
514
|
+
]
|
|
515
|
+
];
|
|
516
|
+
for (const [name, keys, message] of cases) {
|
|
517
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
518
|
+
const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, compatibleCreate(owner)), TypeError, message));
|
|
519
|
+
assertEquals(bodies.length, 0, name);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
test("compatible and ap: IDs of the same DID can be mixed", async () => {
|
|
523
|
+
const owner = await didKey();
|
|
524
|
+
const activities = [
|
|
525
|
+
compatibleCreate(owner, void 0, { id: parseIri(`ap://${owner.did}/activities/${crypto.randomUUID()}`) }),
|
|
526
|
+
compatibleCreate(owner, void 0, { actor: parseIri(`ap+ef61://${owner.did}/actor`) }),
|
|
527
|
+
compatibleCreate(owner, void 0, { id: compatibleId(owner.did, "/activities/1", "https://other.example") })
|
|
528
|
+
];
|
|
529
|
+
for (const activity of activities) {
|
|
530
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
531
|
+
const { bodies } = await capture(async () => ctx.sendActivity([
|
|
532
|
+
rsaKey,
|
|
533
|
+
sender(await didKey()),
|
|
534
|
+
sender(owner)
|
|
535
|
+
], recipient, activity));
|
|
536
|
+
assertEquals(bodies.length, 1, activity.id?.href);
|
|
537
|
+
assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
test("a pre-signed compatible-ID activity is not re-signed", async () => {
|
|
541
|
+
const owner = await didKey();
|
|
542
|
+
const activity = await signObject(compatibleCreate(owner, await signedChild(await didKey())), owner.privateKey, owner.keyId, {
|
|
543
|
+
...options,
|
|
544
|
+
created
|
|
545
|
+
});
|
|
546
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
547
|
+
const { bodies } = await capture(async () => ctx.sendActivity([
|
|
548
|
+
rsaKey,
|
|
549
|
+
sender(await didKey()),
|
|
550
|
+
sender(await didKey())
|
|
551
|
+
], recipient, activity));
|
|
552
|
+
assertEquals(bodies.length, 1);
|
|
553
|
+
assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
|
|
554
|
+
await assertCompoundVerifies(bodies[0]);
|
|
555
|
+
});
|
|
556
|
+
test("forced fanout selects the DID-matching key for a compatible-ID activity", async () => {
|
|
557
|
+
const owner = await didKey();
|
|
558
|
+
const { queue, queued } = createQueue();
|
|
559
|
+
const federation = createTestFederation(queue);
|
|
560
|
+
const ctx = federation.createContext(new URL("https://example.com/"));
|
|
561
|
+
const { bodies } = await capture(async () => {
|
|
562
|
+
await ctx.sendActivity([
|
|
563
|
+
rsaKey,
|
|
564
|
+
sender(await didKey()),
|
|
565
|
+
sender(owner)
|
|
566
|
+
], recipient, compatibleCreate(owner, await signedChild(await didKey())), { fanout: "force" });
|
|
567
|
+
assertEquals(queued.length, 1);
|
|
568
|
+
for (let i = 0; i < queued.length; i++) await federation.processQueuedTask(void 0, queued[i]);
|
|
569
|
+
});
|
|
570
|
+
assertEquals(bodies.length, 1);
|
|
571
|
+
assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
|
|
572
|
+
await assertCompoundVerifies(bodies[0]);
|
|
573
|
+
});
|
|
574
|
+
test("a Fedify inbox accepts an activity Fedify produced for a compatible-ID actor", async () => {
|
|
575
|
+
const owner = await didKey();
|
|
576
|
+
const child = await signedChild(await didKey());
|
|
577
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
578
|
+
const { requests } = await capture(async () => ctx.sendActivity([sender(await didKey()), sender(owner)], recipient, compatibleCreate(owner, child)));
|
|
579
|
+
assertEquals(requests.length, 1);
|
|
580
|
+
let received = 0;
|
|
581
|
+
const receiving = createFederation({
|
|
582
|
+
kv: new MemoryKvStore(),
|
|
583
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
584
|
+
documentLoaderFactory: () => mockDocumentLoader
|
|
585
|
+
});
|
|
586
|
+
receiving.setActorDispatcher("/users/{identifier}", () => null);
|
|
587
|
+
receiving.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
588
|
+
received++;
|
|
589
|
+
});
|
|
590
|
+
const body = await requests[0].text();
|
|
591
|
+
const deliver = (json) => receiving.fetch(new Request(requests[0].url, {
|
|
592
|
+
method: "POST",
|
|
593
|
+
headers: requests[0].headers,
|
|
594
|
+
body: json
|
|
595
|
+
}), { contextData: void 0 });
|
|
596
|
+
assertEquals([(await deliver(body)).status, received], [202, 1]);
|
|
597
|
+
const forged = JSON.parse(body);
|
|
598
|
+
forged.actor = compatibleId((await didKey()).did, "/actor").href;
|
|
599
|
+
assertEquals([(await deliver(JSON.stringify(forged))).status, received], [401, 1]);
|
|
600
|
+
});
|
|
601
|
+
async function captureLogs(run) {
|
|
602
|
+
const records = [];
|
|
603
|
+
await reset();
|
|
604
|
+
try {
|
|
605
|
+
await configure({
|
|
606
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
607
|
+
filters: {},
|
|
608
|
+
loggers: [{
|
|
609
|
+
category: ["logtape", "meta"],
|
|
610
|
+
sinks: []
|
|
611
|
+
}, {
|
|
612
|
+
category: [],
|
|
613
|
+
sinks: ["buffer"],
|
|
614
|
+
lowestLevel: "warning"
|
|
615
|
+
}]
|
|
616
|
+
});
|
|
617
|
+
await run();
|
|
618
|
+
} finally {
|
|
619
|
+
await reset();
|
|
620
|
+
}
|
|
621
|
+
return records.filter((record) => record.category[0] === "fedify");
|
|
622
|
+
}
|
|
623
|
+
test("an activity on another gateway than its compatible-ID actor is warned about", async () => {
|
|
624
|
+
const owner = await didKey();
|
|
625
|
+
const warnings = (activity) => captureLogs(() => assertPortableActorActivity(activity)).then((records) => records.map((r) => r.properties.activityId));
|
|
626
|
+
const other = compatibleId(owner.did, "/activities/1", "https://other.example");
|
|
627
|
+
assertEquals(await warnings(compatibleCreate(owner, void 0, { id: other })), [other.href]);
|
|
628
|
+
assertEquals(await warnings(compatibleCreate(owner)), []);
|
|
629
|
+
assertEquals(await warnings(compatibleCreate(owner, void 0, { id: parseIri(`ap://${owner.did}/activities/1`) })), []);
|
|
630
|
+
assertEquals(await warnings(compatibleCreate(owner, void 0, {
|
|
631
|
+
id: other,
|
|
632
|
+
actor: parseIri(`ap://${owner.did}/actor`)
|
|
633
|
+
})), []);
|
|
634
|
+
});
|
|
635
|
+
test("an unsigned activity with a malformed compatible ID is rejected", async () => {
|
|
636
|
+
const owner = await didKey();
|
|
637
|
+
const ctx = createTestFederation().createContext(new URL("https://example.com/"));
|
|
638
|
+
const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
|
|
639
|
+
id: new URL(compatibleId(owner.did, "/activities/1").href + "?@gateway=https%3A%2F%2Fgw.example"),
|
|
640
|
+
actor: new URL("https://example.com/users/alice")
|
|
641
|
+
})), TypeError, "malformed FEP-ef61 portable ID"));
|
|
642
|
+
assertEquals(bodies.length, 0);
|
|
643
|
+
});
|
|
395
644
|
//#endregion
|
|
396
645
|
export {};
|
|
@@ -7,10 +7,10 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
|
7
7
|
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
8
8
|
import "../std__assert-DvMDRPes.mjs";
|
|
9
9
|
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
10
|
-
import { p as signJsonLd } from "../ld-
|
|
11
|
-
import { s as signRequest } from "../http-
|
|
10
|
+
import { p as signJsonLd } from "../ld-BYZOoDzZ.mjs";
|
|
11
|
+
import { s as signRequest } from "../http-B2ftY0Ha.mjs";
|
|
12
12
|
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { m as handleInbox, n as FederationImpl, o as createFederation } from "../middleware-E_0IH72m.mjs";
|
|
14
14
|
import { t as ActivityListenerSet } from "../activity-listener-C-BSbKI7.mjs";
|
|
15
15
|
import { t as KvKeyCache } from "../keycache-BUDHB5rY.mjs";
|
|
16
16
|
import { Create, CryptographicKey, Person } from "@fedify/vocab";
|
|
@@ -229,11 +229,11 @@ test("Context.getDocumentLoader() signs requests with gateway keys for portable
|
|
|
229
229
|
}
|
|
230
230
|
assertEquals(keyId, gatewayKeyId.href);
|
|
231
231
|
});
|
|
232
|
-
async function actorDocument(gateways) {
|
|
233
|
-
const keys = await createTestFederation(() =>
|
|
232
|
+
async function actorDocument(gateways, id = actorId, signed = true) {
|
|
233
|
+
const keys = await createTestFederation(() => id).createContext(new URL(gateway)).getActorKeyPairs("alice");
|
|
234
234
|
const json = await new Person({
|
|
235
|
-
id
|
|
236
|
-
inbox: parseIri(`${
|
|
235
|
+
id,
|
|
236
|
+
inbox: parseIri(`${id.href}/inbox`),
|
|
237
237
|
gateways: gateways.map((g) => new URL(g)),
|
|
238
238
|
publicKey: keys[0].cryptographicKey,
|
|
239
239
|
assertionMethods: keys.map((k) => k.multikey)
|
|
@@ -242,7 +242,7 @@ async function actorDocument(gateways) {
|
|
|
242
242
|
contextLoader: mockDocumentLoader
|
|
243
243
|
});
|
|
244
244
|
json["@context"] = [...json["@context"], "https://w3id.org/security/data-integrity/v1"];
|
|
245
|
-
return await sign(json);
|
|
245
|
+
return signed ? await sign(json) : json;
|
|
246
246
|
}
|
|
247
247
|
function createLoader(responses) {
|
|
248
248
|
return (url) => {
|
|
@@ -255,8 +255,8 @@ function createLoader(responses) {
|
|
|
255
255
|
});
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
|
-
async function deliver(body, gateways, { kv = new MemoryKvStore(), httpSignature = true } = {}) {
|
|
259
|
-
const documentLoader = createLoader({ [compatibleActorId(gateway)]: await actorDocument(gateways) });
|
|
258
|
+
async function deliver(body, gateways, { kv = new MemoryKvStore(), httpSignature = true, actor } = {}) {
|
|
259
|
+
const documentLoader = createLoader({ [compatibleActorId(gateway)]: actor ?? await actorDocument(gateways) });
|
|
260
260
|
const unsigned = new Request("https://local.example/inbox", {
|
|
261
261
|
method: "POST",
|
|
262
262
|
body: JSON.stringify(body),
|
|
@@ -357,7 +357,68 @@ test("handleInbox() does not take Linked Data Signatures for portable actors", a
|
|
|
357
357
|
status: 401,
|
|
358
358
|
dispatched: 0
|
|
359
359
|
});
|
|
360
|
-
assertEquals(lastResponseText, "
|
|
360
|
+
assertEquals(lastResponseText, "Failed to verify the request signature.");
|
|
361
|
+
});
|
|
362
|
+
const otherDid = await exportDidKey((await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])).publicKey);
|
|
363
|
+
function compatibleCreateJson(id, activityDid = did) {
|
|
364
|
+
return {
|
|
365
|
+
"@context": portableContext,
|
|
366
|
+
id: `${gateway}/.well-known/apgateway/${activityDid}/activities/${id}`,
|
|
367
|
+
type: "Create",
|
|
368
|
+
actor: compatibleActorId(gateway),
|
|
369
|
+
object: "https://example.com/notes/1"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
test("handleInbox() does not trust compatible-ID actors by web origin", async () => {
|
|
373
|
+
const id = new URL(compatibleActorId(gateway));
|
|
374
|
+
for (const signed of [true, false]) {
|
|
375
|
+
const actor = await actorDocument([gateway], id, signed);
|
|
376
|
+
assertEquals(await deliver(compatibleCreateJson(`unsigned-${signed}`), [gateway], { actor }), {
|
|
377
|
+
status: 401,
|
|
378
|
+
dispatched: 0
|
|
379
|
+
}, `actor document signed: ${signed}`);
|
|
380
|
+
}
|
|
381
|
+
assertEquals(await deliver(await sign(compatibleCreateJson("signed")), [gateway], { actor: await actorDocument([gateway], id) }), {
|
|
382
|
+
status: 202,
|
|
383
|
+
dispatched: 1
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
test("handleInbox() requires proofs of compatible-ID actors", async () => {
|
|
387
|
+
assertEquals(await deliver(compatibleCreateJson("unsigned"), [gateway]), {
|
|
388
|
+
status: 401,
|
|
389
|
+
dispatched: 0
|
|
390
|
+
});
|
|
391
|
+
assertEquals(await deliver(await sign(compatibleCreateJson("signed")), [gateway]), {
|
|
392
|
+
status: 202,
|
|
393
|
+
dispatched: 1
|
|
394
|
+
});
|
|
395
|
+
assertEquals(await deliver(await sign(compatibleCreateJson("unsigned-request")), [gateway], { httpSignature: false }), {
|
|
396
|
+
status: 202,
|
|
397
|
+
dispatched: 1
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
test("handleInbox() checks the DIDs of compatible activity IDs", async () => {
|
|
401
|
+
for (const actor of [actorId.href, compatibleActorId(gateway)]) assertEquals(await deliver(await sign({
|
|
402
|
+
...compatibleCreateJson("forged", otherDid),
|
|
403
|
+
actor
|
|
404
|
+
}), [gateway]), {
|
|
405
|
+
status: 401,
|
|
406
|
+
dispatched: 0
|
|
407
|
+
}, actor);
|
|
408
|
+
assertEquals(await deliver(await sign({
|
|
409
|
+
...compatibleCreateJson("forged-ap"),
|
|
410
|
+
id: `ap://${otherDid}/activities/forged-ap`
|
|
411
|
+
}), [gateway]), {
|
|
412
|
+
status: 401,
|
|
413
|
+
dispatched: 0
|
|
414
|
+
});
|
|
415
|
+
assertEquals(await deliver(await sign({
|
|
416
|
+
...compatibleCreateJson("malformed"),
|
|
417
|
+
id: `https://user@example.com/.well-known/apgateway/${did}/activities/2`
|
|
418
|
+
}), [gateway]), {
|
|
419
|
+
status: 401,
|
|
420
|
+
dispatched: 0
|
|
421
|
+
});
|
|
361
422
|
});
|
|
362
423
|
//#endregion
|
|
363
424
|
export {};
|
|
@@ -8,11 +8,11 @@ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
|
8
8
|
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
9
9
|
import "../std__assert-DvMDRPes.mjs";
|
|
10
10
|
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
11
|
-
import { p as signJsonLd } from "../ld-
|
|
12
|
-
import {
|
|
13
|
-
import { s as signRequest, u as verifyRequestDetailed } from "../http-
|
|
11
|
+
import { p as signJsonLd } from "../ld-BYZOoDzZ.mjs";
|
|
12
|
+
import { i as signObject } from "../proof-DVXC6nYz.mjs";
|
|
13
|
+
import { s as signRequest, u as verifyRequestDetailed } from "../http-B2ftY0Ha.mjs";
|
|
14
14
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import { l as forwardPortableInboxActivity, o as createFederation, u as resolvePortableInboxForwardingOptions } from "../middleware-E_0IH72m.mjs";
|
|
16
16
|
import { Create, Follow, Note, Person, Tombstone } from "@fedify/vocab";
|
|
17
17
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
18
18
|
import { exportDidKey, formatIri, parseIri } from "@fedify/vocab-runtime";
|
|
@@ -522,6 +522,118 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
|
|
|
522
522
|
esm_default.hardReset();
|
|
523
523
|
}
|
|
524
524
|
});
|
|
525
|
+
function compatibleAlice(path = "/users/alice", { authority = did, origin = LOCAL, gateways = [
|
|
526
|
+
LOCAL,
|
|
527
|
+
GATEWAY2,
|
|
528
|
+
GATEWAY3
|
|
529
|
+
] } = {}) {
|
|
530
|
+
return {
|
|
531
|
+
id: new URL(`${origin}/.well-known/apgateway/${authority}${path}`),
|
|
532
|
+
inbox: new URL(inboxUrl(authority, `${path}/inbox`, origin)),
|
|
533
|
+
gateways: gateways.map((g) => new URL(g))
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
test("Federation.fetch() delivers to portable inboxes of compatible-ID actors", async (t) => {
|
|
537
|
+
const apAlice = parseIri(`ap+ef61://${did}/users/alice`);
|
|
538
|
+
const apInbox = parseIri(`ap+ef61://${did}/users/alice/inbox`);
|
|
539
|
+
const cases = [
|
|
540
|
+
[
|
|
541
|
+
"compatible actor and inbox",
|
|
542
|
+
compatibleAlice(),
|
|
543
|
+
""
|
|
544
|
+
],
|
|
545
|
+
[
|
|
546
|
+
"on another gateway",
|
|
547
|
+
compatibleAlice(void 0, { origin: GATEWAY2 }),
|
|
548
|
+
""
|
|
549
|
+
],
|
|
550
|
+
[
|
|
551
|
+
"compatible actor and ap: inbox",
|
|
552
|
+
{
|
|
553
|
+
...compatibleAlice(),
|
|
554
|
+
inbox: apInbox
|
|
555
|
+
},
|
|
556
|
+
""
|
|
557
|
+
],
|
|
558
|
+
[
|
|
559
|
+
"ap: actor and compatible inbox",
|
|
560
|
+
{
|
|
561
|
+
...compatibleAlice(),
|
|
562
|
+
id: apAlice
|
|
563
|
+
},
|
|
564
|
+
""
|
|
565
|
+
],
|
|
566
|
+
[
|
|
567
|
+
"compatible inbox with a query",
|
|
568
|
+
{
|
|
569
|
+
...compatibleAlice(),
|
|
570
|
+
inbox: new URL(inboxUrl() + "?page=1")
|
|
571
|
+
},
|
|
572
|
+
"?page=1"
|
|
573
|
+
]
|
|
574
|
+
];
|
|
575
|
+
for (const [name, alice, query] of cases) await t.step(name, async () => {
|
|
576
|
+
const queue = new RecordingQueue();
|
|
577
|
+
const { federation, received } = setup({
|
|
578
|
+
queue,
|
|
579
|
+
actor: (_ctx, identifier) => identifier === "alice" ? new Person(alice) : null
|
|
580
|
+
});
|
|
581
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
582
|
+
assertEquals(received.length, 1);
|
|
583
|
+
assertEquals(received[0].recipient, "alice");
|
|
584
|
+
assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3) + query, inboxUrl(did, "/users/alice/inbox", GATEWAY2) + query]);
|
|
585
|
+
});
|
|
586
|
+
const json = await signedFollow();
|
|
587
|
+
const rejections = [
|
|
588
|
+
["DID mismatch", new Person(compatibleAlice(void 0, { authority: otherDid }))],
|
|
589
|
+
["inbox mismatch", new Person({
|
|
590
|
+
...compatibleAlice(),
|
|
591
|
+
inbox: new URL(inboxUrl(did, "/users/alice/other-inbox"))
|
|
592
|
+
})],
|
|
593
|
+
["this server is not a gateway of the actor", new Person(compatibleAlice(void 0, { gateways: [GATEWAY2] }))],
|
|
594
|
+
["malformed actor ID", new Person({
|
|
595
|
+
...compatibleAlice(),
|
|
596
|
+
id: new URL(compatibleAlice().id.href + "?@gateway=https%3A%2F%2Fexample.com")
|
|
597
|
+
})],
|
|
598
|
+
["malformed inbox ID", new Person({
|
|
599
|
+
...compatibleAlice(),
|
|
600
|
+
inbox: new URL(inboxUrl() + "?@gateway=https%3A%2F%2Fexample.com")
|
|
601
|
+
})]
|
|
602
|
+
];
|
|
603
|
+
for (const [name, actor] of rejections) await t.step(name, async () => {
|
|
604
|
+
const { federation, received } = setup({ actor: (_ctx, identifier) => identifier === "alice" ? actor : null });
|
|
605
|
+
assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 404);
|
|
606
|
+
assertEquals(received.length, 0);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
test("forwardPortableInboxActivity() deduplicates compatible activity IDs", async () => {
|
|
610
|
+
const kv = new MemoryKvStore();
|
|
611
|
+
const queue = new RecordingQueue();
|
|
612
|
+
const forward = (activityId) => forwardPortableInboxActivity({
|
|
613
|
+
recipient: {
|
|
614
|
+
actorId: compatibleAlice().id,
|
|
615
|
+
inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
|
|
616
|
+
canonicalInboxId: `ap+ef61://${did}/users/alice/inbox`,
|
|
617
|
+
gateways: [new URL(LOCAL), new URL(GATEWAY2)]
|
|
618
|
+
},
|
|
619
|
+
activity: { id: activityId.href },
|
|
620
|
+
activityId,
|
|
621
|
+
activityType: "https://www.w3.org/ns/activitystreams#Follow",
|
|
622
|
+
excludedOrigins: [LOCAL],
|
|
623
|
+
baseUrl: LOCAL,
|
|
624
|
+
kv,
|
|
625
|
+
kvPrefix: ["_fedify", "portableInboxForwarding"],
|
|
626
|
+
outboxQueue: queue
|
|
627
|
+
});
|
|
628
|
+
const path = `${did}/follows/compatible`;
|
|
629
|
+
assertEquals((await forward(new URL(`${LOCAL}/.well-known/apgateway/${path}`))).length, 1);
|
|
630
|
+
for (const id of [
|
|
631
|
+
new URL(`${GATEWAY2}/.well-known/apgateway/${path}`),
|
|
632
|
+
new URL(`${LOCAL}/.well-known/apgateway/${path}?x=1`),
|
|
633
|
+
parseIri(`ap://${path}`)
|
|
634
|
+
]) assertEquals(await forward(id), [], id.href);
|
|
635
|
+
assertEquals(queue.outbox.length, 1);
|
|
636
|
+
});
|
|
525
637
|
const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
|
|
526
638
|
const rsaGatewayKeyPair = await crypto.subtle.generateKey({
|
|
527
639
|
name: "RSASSA-PKCS1-v1_5",
|