@fedify/fedify 2.4.0-dev.2115 → 2.4.0-dev.2127
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-CZcBWrBd.mjs → builder-B2bgGYbP.mjs} +3 -3
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-CDuP9MIX.mjs → compound-proof-SVlHSzV7.mjs} +2 -2
- package/dist/{context-SMNOiiY6.d.cts → context-Bm6ldGvx.d.cts} +30 -4
- package/dist/{context-tOiJfBoD.d.ts → context-Ci5vuUr_.d.ts} +30 -4
- package/dist/{context-CoNRxGYp.mjs → context-DfSVPcJ8.mjs} +6 -3
- package/dist/{deno-CCABdw7r.mjs → deno-Be9xoxps.mjs} +1 -1
- package/dist/{docloader-Bby5g2iu.mjs → docloader-Dtuyb0ps.mjs} +7 -7
- 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 +4 -4
- package/dist/federation/handler.test.mjs +6 -6
- 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 +8 -8
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +5 -5
- package/dist/federation/portable-actor.test.mjs +52 -5
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +358 -0
- package/dist/federation/portable-gateway-keys.test.mjs +4 -4
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable.test.mjs +127 -30
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +2 -2
- package/dist/federation/tasks/tasks.test.mjs +3 -3
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-DYr384xz.cjs → federation-B6IHuS6n.cjs} +64 -28
- package/dist/{http-BcLnr2do.mjs → http-BFeK9sBg.mjs} +4 -4
- package/dist/{http-Ce6_Lf7k.js → http-Rmbiu-5S.js} +1 -1
- package/dist/{http-B4mQvNcv.cjs → http-o8l4QeUY.cjs} +1 -1
- package/dist/{key-CkNxztNV.mjs → key-Cc1b6pW9.mjs} +2 -2
- package/dist/{kv-cache-Dqp_E-1g.cjs → kv-cache-BUkgZAcX.cjs} +6 -6
- package/dist/{kv-cache-DwzXzchj.js → kv-cache-BweDce2I.js} +6 -6
- package/dist/{kv-cache-iFaRIcIL.mjs → kv-cache-tLKBfxHc.mjs} +1 -1
- package/dist/{ld-CnT0f1nU.mjs → ld-DQNTIrAe.mjs} +3 -3
- package/dist/{metrics-em_7lwhG.mjs → metrics-DDE9S63Q.mjs} +1 -1
- package/dist/{middleware-BpwKvOpU.mjs → middleware-BdhvduR9.mjs} +72 -35
- package/dist/{middleware-U9IuzvDO.mjs → middleware-CsQorLC_.mjs} +1 -1
- package/dist/{middleware-DhIBngkj.js → middleware-K9aLnf57.js} +62 -26
- package/dist/{mod-DnElMslb.d.cts → mod-AVir5nVI.d.cts} +1 -1
- package/dist/{mod-DMdGFMEl.d.cts → mod-Byt5tvAS.d.cts} +13 -2
- package/dist/{mod-CTh7ijx5.d.ts → mod-Co53r-VP.d.ts} +13 -2
- package/dist/{mod-COWgSo2v.d.ts → mod-eVOUPbQE.d.ts} +1 -1
- package/dist/mod.cjs +8 -8
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/{owner-9LiHmOv1.mjs → owner-CCfdL83M.mjs} +3 -3
- package/dist/{sig-Bj3fHnv0.cjs → portable-collection-DXogVs__.cjs} +220 -2
- package/dist/portable-collection-DdXsKBg4.js +623 -0
- package/dist/{sig-DN9pOglI.js → portable-collection-WXaHkNqC.mjs} +5 -4
- package/dist/{portable-key-9ck61lI0.mjs → portable-key-C-fPq_Yf.mjs} +1 -1
- package/dist/{proof-xdPYGPaA.mjs → proof-CW8k0R1G.mjs} +4 -4
- package/dist/{send-Bf8rSWch.mjs → send--FORaIc4.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 +2 -2
- package/dist/sig/mod.cjs +5 -6
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +2 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +20 -431
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/{temporal-tAtwv6vX.mjs → temporal-CffY_XOJ.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +30 -4
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-BWq1dCzx.mjs → testing-otQhu4aD.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +81 -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 +6 -6
- package/dist/owner-BtoMCv6Z.cjs +0 -225
- package/dist/owner-Cb3ytrlq.js +0 -215
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "temporal-polyfill";
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
@@ -8,10 +8,10 @@ import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.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 { i as signObject, s as verifyPortableObjectProof } from "../proof-
|
|
12
|
-
import { d as verifyRequest, l as signRequest } from "../http-
|
|
11
|
+
import { i as signObject, s as verifyPortableObjectProof } from "../proof-CW8k0R1G.mjs";
|
|
12
|
+
import { d as verifyRequest, l as signRequest } from "../http-BFeK9sBg.mjs";
|
|
13
13
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
14
|
-
import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-
|
|
14
|
+
import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-BdhvduR9.mjs";
|
|
15
15
|
import { n as RouterError } from "../router-u5O5KCQr.mjs";
|
|
16
16
|
import { configure, reset } from "@logtape/logtape";
|
|
17
17
|
import { Note, Person, Tombstone, lookupObject } from "@fedify/vocab";
|
|
@@ -53,6 +53,8 @@ const users = {
|
|
|
53
53
|
tampered: did,
|
|
54
54
|
https: did,
|
|
55
55
|
tombstone: did,
|
|
56
|
+
"signed-tombstone": did,
|
|
57
|
+
"wrong-key-tombstone": did,
|
|
56
58
|
"mutated-id": did,
|
|
57
59
|
compatible: did,
|
|
58
60
|
"a b/ç": did
|
|
@@ -88,6 +90,12 @@ function createTestFederation(options = {}) {
|
|
|
88
90
|
return await sign(actor.clone({ id: toCompatibleEf61Id(actor.id, ORIGIN) }));
|
|
89
91
|
}
|
|
90
92
|
case "tombstone": return new Tombstone({ id: ctx.getPortableActorUri(identifier, userDid) });
|
|
93
|
+
case "signed-tombstone": return await sign(new Tombstone({
|
|
94
|
+
id: ctx.getPortableActorUri(identifier, userDid),
|
|
95
|
+
formerType: Person,
|
|
96
|
+
deleted: Temporal.Instant.from("2026-01-01T00:00:00Z")
|
|
97
|
+
}));
|
|
98
|
+
case "wrong-key-tombstone": return await sign(new Tombstone({ id: ctx.getPortableActorUri(identifier, userDid) }), otherKeyPair.privateKey, otherKeyId);
|
|
91
99
|
case "unsigned": return await portableActor(ctx, identifier, userDid);
|
|
92
100
|
case "wrong-key": return await sign(await portableActor(ctx, identifier, userDid), otherKeyPair.privateKey, otherKeyId);
|
|
93
101
|
case "tampered": return (await sign(await portableActor(ctx, identifier, userDid))).clone({ name: "Tampered" });
|
|
@@ -260,6 +268,7 @@ test("Federation.fetch() serves portable actors through the actor dispatcher", a
|
|
|
260
268
|
gatewayRequest("/users/missing"),
|
|
261
269
|
gatewayRequest("/users/https"),
|
|
262
270
|
gatewayRequest("/users/tombstone"),
|
|
271
|
+
gatewayRequest("/users/tombstone", { method: "HEAD" }),
|
|
263
272
|
gatewayRequest("/users/alice", { authority: otherDid }),
|
|
264
273
|
gatewayRequest("/users/mutated-id")
|
|
265
274
|
]) {
|
|
@@ -278,13 +287,36 @@ test("Federation.fetch() serves portable actors through the actor dispatcher", a
|
|
|
278
287
|
for (const identifier of [
|
|
279
288
|
"unsigned",
|
|
280
289
|
"wrong-key",
|
|
281
|
-
"tampered"
|
|
290
|
+
"tampered",
|
|
291
|
+
"wrong-key-tombstone"
|
|
282
292
|
]) {
|
|
283
293
|
const response = await federation.fetch(gatewayRequest(`/users/${identifier}`), { contextData: void 0 });
|
|
284
294
|
assertEquals(response.status, 500, identifier);
|
|
285
295
|
assertEquals(await response.text(), "Internal server error.");
|
|
286
296
|
}
|
|
287
297
|
});
|
|
298
|
+
await t.step("serves a signed tombstone with 410 Gone", async () => {
|
|
299
|
+
const response = await federation.fetch(gatewayRequest("/users/signed-tombstone"), { contextData: void 0 });
|
|
300
|
+
assertEquals(response.status, 410);
|
|
301
|
+
assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
|
|
302
|
+
assertEquals(response.headers.get("Vary"), "Accept");
|
|
303
|
+
const json = await response.json();
|
|
304
|
+
assertEquals(json.type, "Tombstone");
|
|
305
|
+
assertEquals(json.id, `ap+ef61://${did}/users/signed-tombstone`);
|
|
306
|
+
assertEquals(json.deleted, "2026-01-01T00:00:00Z");
|
|
307
|
+
assertEquals((await verifyPortableObjectProof(json, { contextLoader: mockDocumentLoader })).verified, true);
|
|
308
|
+
});
|
|
309
|
+
await t.step("responds to HEAD for a tombstone without a body", async () => {
|
|
310
|
+
const response = await federation.fetch(gatewayRequest("/users/signed-tombstone", { method: "HEAD" }), { contextData: void 0 });
|
|
311
|
+
assertEquals(response.status, 410);
|
|
312
|
+
assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
|
|
313
|
+
assertEquals(response.body, null);
|
|
314
|
+
});
|
|
315
|
+
await t.step("keeps ordinary tombstone requests unchanged", async () => {
|
|
316
|
+
const response = await federation.fetch(new Request(`${ORIGIN}/users/signed-tombstone`, { headers: { Accept: ACCEPT } }), { contextData: void 0 });
|
|
317
|
+
assertEquals(response.status, 410);
|
|
318
|
+
assertEquals(response.headers.get("Content-Type"), "application/activity+json");
|
|
319
|
+
});
|
|
288
320
|
await t.step("keeps ordinary actor requests unchanged", async () => {
|
|
289
321
|
const response = await federation.fetch(new Request(`${ORIGIN}/users/https`, { headers: { Accept: ACCEPT } }), { contextData: void 0 });
|
|
290
322
|
assertEquals(response.status, 200);
|
|
@@ -329,6 +361,21 @@ test("Federation.fetch() authorizes portable actor requests", async (t) => {
|
|
|
329
361
|
})).status, 401);
|
|
330
362
|
assertEquals(unauthorized, true);
|
|
331
363
|
});
|
|
364
|
+
await t.step("authorizes before serving a tombstone", async () => {
|
|
365
|
+
for (const identifier of ["signed-tombstone", "tombstone"]) {
|
|
366
|
+
identifiers.length = 0;
|
|
367
|
+
let unauthorized = false;
|
|
368
|
+
assertEquals((await federation.fetch(gatewayRequest(`/users/${identifier}`), {
|
|
369
|
+
contextData: void 0,
|
|
370
|
+
onUnauthorized() {
|
|
371
|
+
unauthorized = true;
|
|
372
|
+
return new Response("Unauthorized", { status: 401 });
|
|
373
|
+
}
|
|
374
|
+
})).status, 401, identifier);
|
|
375
|
+
assertEquals(unauthorized, true, identifier);
|
|
376
|
+
assertEquals(identifiers, [identifier]);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
332
379
|
await t.step("denies a request whose target was tampered with", async () => {
|
|
333
380
|
const signed = await signRequest(gatewayRequest("/users/alice"), rsaPrivateKey3, rsaPublicKey3.id);
|
|
334
381
|
assertEquals((await federation.fetch(new Request(gatewayUrl("/actor"), { headers: signed.headers }), { contextData: void 0 })).status, 401);
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
6
|
+
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
7
|
+
import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
|
|
8
|
+
import "../std__assert-DvMDRPes.mjs";
|
|
9
|
+
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
10
|
+
import { n as exportJwk } from "../key-Cc1b6pW9.mjs";
|
|
11
|
+
import { o as verifyObject, s as verifyPortableObjectProof } from "../proof-CW8k0R1G.mjs";
|
|
12
|
+
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
13
|
+
import { n as FederationImpl } from "../middleware-BdhvduR9.mjs";
|
|
14
|
+
import { Create, DataIntegrityProof, Note, OrderedCollection, Person, traverseCollection } from "@fedify/vocab";
|
|
15
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
16
|
+
import { FetchError, encodeMultibase, exportDidKey } from "@fedify/vocab-runtime";
|
|
17
|
+
import serialize from "json-canon";
|
|
18
|
+
//#region src/federation/portable-chain.test.ts
|
|
19
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
20
|
+
const keyId = `${did}#${did.slice(8)}`;
|
|
21
|
+
const GATEWAY = "https://gw.example";
|
|
22
|
+
const context = [
|
|
23
|
+
"https://www.w3.org/ns/activitystreams",
|
|
24
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
25
|
+
"https://w3id.org/fep/ef61"
|
|
26
|
+
];
|
|
27
|
+
function compatible(path, gateway = GATEWAY) {
|
|
28
|
+
return `${gateway}/.well-known/apgateway/${did}${path}`;
|
|
29
|
+
}
|
|
30
|
+
async function sign(document, proofOptions = {}) {
|
|
31
|
+
const proofConfig = {
|
|
32
|
+
"@context": document["@context"],
|
|
33
|
+
...proofOptions,
|
|
34
|
+
type: "DataIntegrityProof",
|
|
35
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
36
|
+
verificationMethod: keyId,
|
|
37
|
+
proofPurpose: "assertionMethod",
|
|
38
|
+
created: "2023-02-24T23:36:38Z"
|
|
39
|
+
};
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
const digest = /* @__PURE__ */ new Uint8Array(64);
|
|
42
|
+
digest.set(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)))), 0);
|
|
43
|
+
digest.set(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(serialize(document)))), 32);
|
|
44
|
+
const signature = await crypto.subtle.sign("Ed25519", ed25519PrivateKey, digest);
|
|
45
|
+
return {
|
|
46
|
+
...document,
|
|
47
|
+
proof: {
|
|
48
|
+
...proofConfig,
|
|
49
|
+
proofValue: new TextDecoder().decode(encodeMultibase("base58btc", new Uint8Array(signature)))
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function note(path, content = "A portable note") {
|
|
54
|
+
return {
|
|
55
|
+
"@context": context,
|
|
56
|
+
id: compatible(path),
|
|
57
|
+
type: "Note",
|
|
58
|
+
attributedTo: compatible("/actor"),
|
|
59
|
+
content
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function activity(object, id = compatible("/activities/1")) {
|
|
63
|
+
return {
|
|
64
|
+
"@context": context,
|
|
65
|
+
id,
|
|
66
|
+
type: "Create",
|
|
67
|
+
actor: compatible("/actor"),
|
|
68
|
+
object
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function actor() {
|
|
72
|
+
return {
|
|
73
|
+
"@context": context,
|
|
74
|
+
id: compatible("/actor"),
|
|
75
|
+
type: "Person",
|
|
76
|
+
inbox: compatible("/actor/inbox"),
|
|
77
|
+
outbox: compatible("/actor/outbox"),
|
|
78
|
+
gateways: [GATEWAY]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createLoader(responses) {
|
|
82
|
+
const fetched = [];
|
|
83
|
+
const loader = (url) => {
|
|
84
|
+
url = url.replace(/^ap\+ef61:/, "ap:");
|
|
85
|
+
fetched.push(url);
|
|
86
|
+
const document = responses[url];
|
|
87
|
+
if (document == null) {
|
|
88
|
+
if (!URL.canParse(url)) return Promise.reject(/* @__PURE__ */ new Error(`Not found: ${url}`));
|
|
89
|
+
if (!url.includes("/.well-known/apgateway/")) return mockDocumentLoader(url);
|
|
90
|
+
return Promise.reject(new FetchError(url, "HTTP 404", new Response(null, { status: 404 })));
|
|
91
|
+
}
|
|
92
|
+
return Promise.resolve({
|
|
93
|
+
contextUrl: null,
|
|
94
|
+
documentUrl: url,
|
|
95
|
+
document: structuredClone(document)
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
return Object.assign(loader, { fetched });
|
|
99
|
+
}
|
|
100
|
+
function createFederation(documentLoader, queue) {
|
|
101
|
+
return new FederationImpl({
|
|
102
|
+
kv: new MemoryKvStore(),
|
|
103
|
+
queue,
|
|
104
|
+
manuallyStartQueue: true,
|
|
105
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
106
|
+
documentLoaderFactory: () => documentLoader
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
test("Context.verifyPortableObject", async (t) => {
|
|
110
|
+
const signed = await sign(note("/notes/1"));
|
|
111
|
+
const ctx = createFederation(createLoader({})).createContext(new URL("https://example.com/"));
|
|
112
|
+
await t.step("is an own property that survives spreading", () => {
|
|
113
|
+
assert(Object.hasOwn(ctx, "verifyPortableObject"));
|
|
114
|
+
assert({ ...ctx }.verifyPortableObject === ctx.verifyPortableObject);
|
|
115
|
+
assert(ctx.clone(void 0).verifyPortableObject != null);
|
|
116
|
+
});
|
|
117
|
+
await t.step("verifies portable objects with the context's loaders", async () => {
|
|
118
|
+
assertEquals((await ctx.verifyPortableObject(signed, {})).verified, true);
|
|
119
|
+
assertEquals((await ctx.verifyPortableObject(note("/notes/1"), {})).verified, false);
|
|
120
|
+
assertEquals((await ctx.verifyPortableObject({
|
|
121
|
+
...signed,
|
|
122
|
+
content: "Forged"
|
|
123
|
+
}, {})).verified, false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
test("accessors given a Context verify references of compatible-ID objects", async (t) => {
|
|
127
|
+
const signed = await sign(note("/notes/1"));
|
|
128
|
+
const create = () => Create.fromJsonLd(activity(compatible("/notes/1")), { contextLoader: mockDocumentLoader });
|
|
129
|
+
await t.step("a signed object at the reference", async () => {
|
|
130
|
+
const loader = createLoader({ [compatible("/notes/1")]: signed });
|
|
131
|
+
const ctx = createFederation(loader).createContext(new URL("https://example.com/"));
|
|
132
|
+
const object = await (await create()).getObject(ctx);
|
|
133
|
+
assertInstanceOf(object, Note);
|
|
134
|
+
assertEquals(object.content, "A portable note");
|
|
135
|
+
assertEquals(loader.fetched, [compatible("/notes/1")]);
|
|
136
|
+
});
|
|
137
|
+
await t.step("an unsigned object at the reference", async () => {
|
|
138
|
+
const ctx = createFederation(createLoader({ [compatible("/notes/1")]: note("/notes/1") })).createContext(new URL("https://example.com/"));
|
|
139
|
+
assertEquals(await (await create()).getObject(ctx), null);
|
|
140
|
+
});
|
|
141
|
+
await t.step("a forged object at the reference", async () => {
|
|
142
|
+
const ctx = createFederation(createLoader({ [compatible("/notes/1")]: {
|
|
143
|
+
...signed,
|
|
144
|
+
content: "Forged"
|
|
145
|
+
} })).createContext(new URL("https://example.com/"));
|
|
146
|
+
assertEquals(await (await create()).getObject(ctx), null);
|
|
147
|
+
});
|
|
148
|
+
await t.step("an ordinary object's compatible reference", async () => {
|
|
149
|
+
const loader = createLoader({ [compatible("/notes/1")]: note("/notes/1") });
|
|
150
|
+
const ctx = createFederation(loader).createContext(new URL("https://example.com/"));
|
|
151
|
+
const create = await Create.fromJsonLd(activity(compatible("/notes/1"), "https://example.com/activities/1"), { contextLoader: mockDocumentLoader });
|
|
152
|
+
assertEquals(await create.getObject(ctx), null);
|
|
153
|
+
assertInstanceOf(await create.getObject({
|
|
154
|
+
documentLoader: loader,
|
|
155
|
+
contextLoader: mockDocumentLoader
|
|
156
|
+
}), Note);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
function createRecordingQueue() {
|
|
160
|
+
const queued = [];
|
|
161
|
+
return {
|
|
162
|
+
queued,
|
|
163
|
+
queue: {
|
|
164
|
+
enqueue(message) {
|
|
165
|
+
queued.push(message);
|
|
166
|
+
return Promise.resolve();
|
|
167
|
+
},
|
|
168
|
+
listen() {
|
|
169
|
+
return Promise.resolve();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
test("inbox listeners dereference references of compatible-ID activities without options", async (t) => {
|
|
175
|
+
const signedNote = await sign(note("/notes/1"));
|
|
176
|
+
const signedCreate = await sign(activity(compatible("/notes/1")));
|
|
177
|
+
for (const queued of [false, true]) for (const [label, served] of [["signed", signedNote], ["unsigned", note("/notes/1")]]) await t.step(`${label} object${queued ? " through a queue" : ""}`, async () => {
|
|
178
|
+
const loader = createLoader({ [compatible("/notes/1")]: served });
|
|
179
|
+
const { queue, queued: messages } = createRecordingQueue();
|
|
180
|
+
const federation = createFederation(loader, queued ? queue : void 0);
|
|
181
|
+
const results = [];
|
|
182
|
+
federation.setActorDispatcher("/users/{identifier}", () => null);
|
|
183
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, async (_ctx, create) => {
|
|
184
|
+
const object = await create.getObject();
|
|
185
|
+
assert(object == null || object instanceof Note);
|
|
186
|
+
results.push(object);
|
|
187
|
+
});
|
|
188
|
+
assertEquals((await federation.fetch(new Request("https://example.com/inbox", {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
191
|
+
body: JSON.stringify(signedCreate)
|
|
192
|
+
}), { contextData: void 0 })).status, 202);
|
|
193
|
+
if (queued) {
|
|
194
|
+
assertEquals(messages.length, 1);
|
|
195
|
+
assertEquals(messages[0].type, "inbox");
|
|
196
|
+
await federation.processQueuedTask(void 0, messages[0]);
|
|
197
|
+
}
|
|
198
|
+
assertEquals(results.length, 1);
|
|
199
|
+
if (label === "signed") {
|
|
200
|
+
assertInstanceOf(results[0], Note);
|
|
201
|
+
assertEquals(results[0].content, "A portable note");
|
|
202
|
+
} else assertEquals(results[0], null);
|
|
203
|
+
assertEquals(loader.fetched, [compatible("/notes/1")]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
test("verifyObject() does not leave its proof verifier to returned objects", async () => {
|
|
207
|
+
const proofUrl = `ap://${did}/proofs/1`;
|
|
208
|
+
const { proof, ...document } = await sign(activity(compatible("/notes/1")), { id: proofUrl });
|
|
209
|
+
const referenced = {
|
|
210
|
+
...document,
|
|
211
|
+
"https://w3id.org/security#proof": [{ "@graph": [proof] }, { "@graph": [{ "@id": proofUrl }] }]
|
|
212
|
+
};
|
|
213
|
+
const loader = createLoader({ [proofUrl]: proof });
|
|
214
|
+
const verifier = () => Promise.resolve({ verified: false });
|
|
215
|
+
for (const verifyPortableObject of [void 0, verifier]) {
|
|
216
|
+
const create = await verifyObject(Create, referenced, {
|
|
217
|
+
contextLoader: mockDocumentLoader,
|
|
218
|
+
documentLoader: loader,
|
|
219
|
+
verifyPortableObject
|
|
220
|
+
});
|
|
221
|
+
assertInstanceOf(create, Create);
|
|
222
|
+
assert(loader.fetched.includes(proofUrl));
|
|
223
|
+
const proofs = await Array.fromAsync(create.getProofs());
|
|
224
|
+
assertEquals(proofs.length, 2);
|
|
225
|
+
for (const p of proofs) {
|
|
226
|
+
assertInstanceOf(p, DataIntegrityProof);
|
|
227
|
+
assert(p._verifyPortableObject === verifyPortableObject);
|
|
228
|
+
}
|
|
229
|
+
assert(create._verifyPortableObject === verifyPortableObject);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
test("Context.lookupObject() applies the gateway policy to unsecured collections", async (t) => {
|
|
233
|
+
const outbox = {
|
|
234
|
+
"@context": context,
|
|
235
|
+
id: compatible("/actor/outbox"),
|
|
236
|
+
type: "OrderedCollection",
|
|
237
|
+
attributedTo: compatible("/actor"),
|
|
238
|
+
orderedItems: [compatible("/notes/1")]
|
|
239
|
+
};
|
|
240
|
+
const signedActor = await sign(actor());
|
|
241
|
+
await t.step("from a listed gateway", async () => {
|
|
242
|
+
const ctx = createFederation(createLoader({
|
|
243
|
+
[compatible("/actor")]: signedActor,
|
|
244
|
+
[compatible("/actor/outbox")]: outbox,
|
|
245
|
+
[compatible("/notes/1")]: note("/notes/1")
|
|
246
|
+
})).createContext(new URL("https://example.com/"));
|
|
247
|
+
const collection = await ctx.lookupObject(compatible("/actor/outbox"));
|
|
248
|
+
assertInstanceOf(collection, OrderedCollection);
|
|
249
|
+
assertEquals(await Array.fromAsync(collection.getItems()), []);
|
|
250
|
+
assertEquals(await Array.fromAsync(traverseCollection(collection, ctx)), []);
|
|
251
|
+
assertEquals(await ctx.lookupObject(compatible("/actor/outbox"), { verifyPortableObject: verifyPortableObjectProof }), null);
|
|
252
|
+
});
|
|
253
|
+
await t.step("from an unlisted gateway", async () => {
|
|
254
|
+
const evil = "https://evil.example";
|
|
255
|
+
assertEquals(await createFederation(createLoader({
|
|
256
|
+
[compatible("/actor", evil)]: signedActor,
|
|
257
|
+
[compatible("/actor/outbox", evil)]: {
|
|
258
|
+
...outbox,
|
|
259
|
+
id: compatible("/actor/outbox", evil)
|
|
260
|
+
}
|
|
261
|
+
})).createContext(new URL("https://example.com/")).lookupObject(compatible("/actor/outbox", evil)), null);
|
|
262
|
+
});
|
|
263
|
+
await t.step("with signed items", async () => {
|
|
264
|
+
const collection = await createFederation(createLoader({
|
|
265
|
+
[compatible("/actor")]: signedActor,
|
|
266
|
+
[compatible("/actor/outbox")]: outbox,
|
|
267
|
+
[compatible("/notes/1")]: await sign(note("/notes/1"))
|
|
268
|
+
})).createContext(new URL("https://example.com/")).lookupObject(compatible("/actor/outbox"));
|
|
269
|
+
assertInstanceOf(collection, OrderedCollection);
|
|
270
|
+
const items = await Array.fromAsync(collection.getItems());
|
|
271
|
+
assertEquals(items.length, 1);
|
|
272
|
+
assertInstanceOf(items[0], Note);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
test("traverseCollection() with a Context verifies pages and items of portable collections", async () => {
|
|
276
|
+
const loader = createLoader({
|
|
277
|
+
[compatible("/actor")]: await sign(actor()),
|
|
278
|
+
[compatible("/actor/outbox")]: {
|
|
279
|
+
"@context": context,
|
|
280
|
+
id: compatible("/actor/outbox"),
|
|
281
|
+
type: "OrderedCollection",
|
|
282
|
+
first: compatible("/actor/outbox/1")
|
|
283
|
+
},
|
|
284
|
+
[compatible("/actor/outbox/1")]: {
|
|
285
|
+
"@context": context,
|
|
286
|
+
id: compatible("/actor/outbox/1"),
|
|
287
|
+
type: "OrderedCollectionPage",
|
|
288
|
+
partOf: compatible("/actor/outbox"),
|
|
289
|
+
orderedItems: [compatible("/notes/1"), compatible("/notes/2")]
|
|
290
|
+
},
|
|
291
|
+
[compatible("/notes/1")]: await sign(note("/notes/1")),
|
|
292
|
+
[compatible("/notes/2")]: note("/notes/2", "Unsigned")
|
|
293
|
+
});
|
|
294
|
+
const ctx = createFederation(loader).createContext(new URL("https://example.com/"));
|
|
295
|
+
const outbox = await (await Person.fromJsonLd(actor(), { contextLoader: mockDocumentLoader })).getOutbox(ctx);
|
|
296
|
+
assertInstanceOf(outbox, OrderedCollection);
|
|
297
|
+
const items = await Array.fromAsync(traverseCollection(outbox, ctx));
|
|
298
|
+
assertEquals(items.length, 1);
|
|
299
|
+
assertInstanceOf(items[0], Note);
|
|
300
|
+
assertEquals(items[0].content, "A portable note");
|
|
301
|
+
let error;
|
|
302
|
+
try {
|
|
303
|
+
await items[0].getAttribution({ documentLoader: loader });
|
|
304
|
+
} catch (e) {
|
|
305
|
+
error = e;
|
|
306
|
+
}
|
|
307
|
+
assertInstanceOf(error, TypeError);
|
|
308
|
+
assertInstanceOf(await items[0].getAttribution(ctx), Person);
|
|
309
|
+
});
|
|
310
|
+
test("onOutboxError receives activities that verify portable objects by default", async () => {
|
|
311
|
+
const loader = createLoader({ [compatible("/notes/1")]: await sign(note("/notes/1")) });
|
|
312
|
+
const received = [];
|
|
313
|
+
const federation = new FederationImpl({
|
|
314
|
+
kv: new MemoryKvStore(),
|
|
315
|
+
queue: createRecordingQueue().queue,
|
|
316
|
+
manuallyStartQueue: true,
|
|
317
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
318
|
+
documentLoaderFactory: () => loader,
|
|
319
|
+
authenticatedDocumentLoaderFactory: () => loader,
|
|
320
|
+
onOutboxError: async (_error, activity) => {
|
|
321
|
+
try {
|
|
322
|
+
received.push(await activity?.getObject());
|
|
323
|
+
} catch (error) {
|
|
324
|
+
received.push(error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const message = {
|
|
329
|
+
type: "outbox",
|
|
330
|
+
id: crypto.randomUUID(),
|
|
331
|
+
baseUrl: "https://example.com",
|
|
332
|
+
keys: [{
|
|
333
|
+
keyId: rsaPublicKey2.id.href,
|
|
334
|
+
privateKey: await exportJwk(rsaPrivateKey2)
|
|
335
|
+
}],
|
|
336
|
+
activity: await sign(activity(compatible("/notes/1"))),
|
|
337
|
+
activityId: compatible("/activities/1"),
|
|
338
|
+
activityType: "https://www.w3.org/ns/activitystreams#Create",
|
|
339
|
+
inbox: "https://remote.example/inbox",
|
|
340
|
+
sharedInbox: false,
|
|
341
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
342
|
+
attempt: 0,
|
|
343
|
+
headers: {},
|
|
344
|
+
traceContext: {}
|
|
345
|
+
};
|
|
346
|
+
esm_default.spyGlobal();
|
|
347
|
+
esm_default.post("https://remote.example/inbox", 500);
|
|
348
|
+
try {
|
|
349
|
+
await federation.processQueuedTask(void 0, message);
|
|
350
|
+
} finally {
|
|
351
|
+
esm_default.hardReset();
|
|
352
|
+
}
|
|
353
|
+
assertEquals(received.length, 1);
|
|
354
|
+
assertInstanceOf(received[0], Note);
|
|
355
|
+
assertEquals(loader.fetched, [compatible("/notes/1")]);
|
|
356
|
+
});
|
|
357
|
+
//#endregion
|
|
358
|
+
export {};
|
|
@@ -2,15 +2,15 @@ import "temporal-polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
-
import { r as createRequestContext, t as createInboxContext } from "../context-
|
|
5
|
+
import { r as createRequestContext, t as createInboxContext } from "../context-DfSVPcJ8.mjs";
|
|
6
6
|
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 { l as signRequest } from "../http-
|
|
10
|
+
import { p as signJsonLd } from "../ld-DQNTIrAe.mjs";
|
|
11
|
+
import { l as signRequest } from "../http-BFeK9sBg.mjs";
|
|
12
12
|
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
13
|
-
import { f as handleInbox, n as FederationImpl, o as createFederation } from "../middleware-
|
|
13
|
+
import { f as handleInbox, n as FederationImpl, o as createFederation } from "../middleware-BdhvduR9.mjs";
|
|
14
14
|
import { t as ActivityListenerSet } from "../activity-listener-C-BSbKI7.mjs";
|
|
15
15
|
import { t as KvKeyCache } from "../keycache-BInaECGj.mjs";
|
|
16
16
|
import { Create, CryptographicKey, Person } from "@fedify/vocab";
|
|
@@ -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 { i as signObject } from "../proof-
|
|
13
|
-
import { f as verifyRequestDetailed, l as signRequest } from "../http-
|
|
11
|
+
import { p as signJsonLd } from "../ld-DQNTIrAe.mjs";
|
|
12
|
+
import { i as signObject } from "../proof-CW8k0R1G.mjs";
|
|
13
|
+
import { f as verifyRequestDetailed, l as signRequest } from "../http-BFeK9sBg.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 { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-
|
|
15
|
+
import { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-BdhvduR9.mjs";
|
|
16
16
|
import { Create, CryptographicKey, 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";
|