@fedify/fedify 2.4.0-dev.2099 → 2.4.0-dev.2103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/{builder-Bno54o_-.mjs → builder-CHht75pX.mjs} +2 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-X3SxXVbc.mjs → compound-proof-DKKH_Nr1.mjs} +2 -2
  6. package/dist/{context-MORIQJIh.d.ts → context-CfJE_oQu.d.ts} +73 -11
  7. package/dist/{context-DlZhxdFT.mjs → context-CoNRxGYp.mjs} +2 -1
  8. package/dist/{context-C43spTGI.d.cts → context-mDTxTw-S.d.cts} +73 -11
  9. package/dist/{deno-dxdvl2XZ.mjs → deno-CkjiYK_Z.mjs} +1 -1
  10. package/dist/{docloader-eBxQDsk9.mjs → docloader-C1hAJnWc.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-actor.test.d.mts +2 -0
  25. package/dist/federation/portable-actor.test.mjs +446 -0
  26. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  27. package/dist/federation/portable-inbox.test.mjs +25 -4
  28. package/dist/federation/portable.test.mjs +3 -3
  29. package/dist/federation/send.test.mjs +3 -3
  30. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  31. package/dist/federation/tasks/tasks.test.mjs +3 -3
  32. package/dist/federation/temporal.test.mjs +1 -1
  33. package/dist/federation/webfinger.test.mjs +2 -2
  34. package/dist/{federation-C4Ef3JKa.cjs → federation-C1dL6SiT.cjs} +70 -11
  35. package/dist/{http-DsgawTd8.mjs → http-BU-8fUrz.mjs} +4 -4
  36. package/dist/{http-C8cZTE3Y.cjs → http-Cala2Grs.cjs} +1 -1
  37. package/dist/{http-D-rbr_Sy.js → http-tTlTeFa0.js} +1 -1
  38. package/dist/{key-DYfmjnmp.mjs → key-I8gLBkcf.mjs} +2 -2
  39. package/dist/{kv-cache-Dvd-Q-E2.cjs → kv-cache-Bl5EBFrU.cjs} +1 -1
  40. package/dist/{kv-cache-DIxwMeNi.js → kv-cache-CxQu_cd4.js} +1 -1
  41. package/dist/{kv-cache-EvOgrFsS.mjs → kv-cache-DJFoGRWQ.mjs} +1 -1
  42. package/dist/{ld-B00wWHzc.mjs → ld-wiJbqJHa.mjs} +3 -3
  43. package/dist/{metrics-BP3vdBpb.mjs → metrics-BCAa5-kE.mjs} +1 -1
  44. package/dist/{middleware-CeBVBa61.mjs → middleware-6p2dYEVA.mjs} +1 -1
  45. package/dist/{middleware-CcXMe400.js → middleware-B6IVTVL0.js} +70 -11
  46. package/dist/{middleware-Br1_d-Ga.mjs → middleware-FnITXhtz.mjs} +80 -21
  47. package/dist/{mod-DMI4DbOo.d.cts → mod-B3OzOobN.d.cts} +1 -1
  48. package/dist/{mod-BL5BQHCq.d.ts → mod-BYvPz_T0.d.ts} +1 -1
  49. package/dist/mod.cjs +5 -5
  50. package/dist/mod.d.cts +2 -2
  51. package/dist/mod.d.ts +2 -2
  52. package/dist/mod.js +5 -5
  53. package/dist/nodeinfo/handler.test.mjs +2 -2
  54. package/dist/{owner-BJPDRDRq.mjs → owner-BJp47LxN.mjs} +3 -3
  55. package/dist/{owner-DSzH7wro.cjs → owner-DCt8W_Cv.cjs} +1 -1
  56. package/dist/{owner-U12fDpQW.js → owner-j5yrbXS1.js} +1 -1
  57. package/dist/{portable-key-D0mWbV9G.mjs → portable-key-B5oKE_wZ.mjs} +1 -1
  58. package/dist/{proof-ChjBwnep.mjs → proof-BE4T5zF6.mjs} +4 -4
  59. package/dist/{send-yFX0E-ye.mjs → send-BFITuKHw.mjs} +3 -3
  60. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  61. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  62. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  63. package/dist/sig/compound-proof.test.mjs +2 -2
  64. package/dist/sig/http.test.mjs +2 -2
  65. package/dist/sig/key.test.mjs +1 -1
  66. package/dist/sig/ld.test.mjs +2 -2
  67. package/dist/sig/mod.cjs +3 -3
  68. package/dist/sig/mod.js +3 -3
  69. package/dist/sig/owner.test.mjs +1 -1
  70. package/dist/sig/portable-collection.test.mjs +2 -2
  71. package/dist/sig/portable-key.test.mjs +3 -3
  72. package/dist/sig/proof.test.mjs +3 -3
  73. package/dist/{sig-C85KqAcx.js → sig-CJHdGk9w.js} +2 -2
  74. package/dist/{sig-B689Nj7y.cjs → sig-CRTRoouH.cjs} +2 -2
  75. package/dist/{temporal-Dl9x-Qw4.mjs → temporal-CyDDbE4i.mjs} +1 -1
  76. package/dist/testing/kv.test.mjs +1 -1
  77. package/dist/testing/mod.d.mts +73 -11
  78. package/dist/testing/mod.mjs +2 -2
  79. package/dist/{testing-CO_cEvQc.mjs → testing-BWq1dCzx.mjs} +1 -1
  80. package/dist/utils/docloader.test.mjs +2 -2
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.js +1 -1
  84. package/package.json +7 -7
@@ -0,0 +1,446 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
7
+ import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
8
+ import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
9
+ import "../std__assert-DvMDRPes.mjs";
10
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
11
+ import { i as signObject, s as verifyPortableObjectProof } from "../proof-BE4T5zF6.mjs";
12
+ import { d as verifyRequest, l as signRequest } from "../http-BU-8fUrz.mjs";
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-FnITXhtz.mjs";
15
+ import { n as RouterError } from "../router-u5O5KCQr.mjs";
16
+ import { configure, reset } from "@logtape/logtape";
17
+ import { Note, Person, Tombstone, lookupObject } from "@fedify/vocab";
18
+ import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
+ import { exportDidKey, formatIri, fromCompatibleEf61Id, toCompatibleEf61Id } from "@fedify/vocab-runtime";
20
+ //#region src/federation/portable-actor.test.ts
21
+ const did = await exportDidKey(ed25519PublicKey.publicKey);
22
+ const keyId = new URL(`${did}#${did.slice(8)}`);
23
+ const otherKeyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
24
+ const otherDid = await exportDidKey(otherKeyPair.publicKey);
25
+ const otherKeyId = new URL(`${otherDid}#${otherDid.slice(8)}`);
26
+ const ORIGIN = "https://example.com";
27
+ const ACCEPT = "application/activity+json";
28
+ function gatewayUrl(path, authority = did) {
29
+ return `${ORIGIN}/.well-known/apgateway/${authority}${path}`;
30
+ }
31
+ function gatewayRequest(path, init = {}) {
32
+ const { authority, ...rest } = init;
33
+ return new Request(gatewayUrl(path, authority), {
34
+ ...rest,
35
+ headers: {
36
+ Accept: ACCEPT,
37
+ ...rest.headers
38
+ }
39
+ });
40
+ }
41
+ async function sign(object, privateKey = ed25519PrivateKey, key = keyId) {
42
+ return await signObject(object, privateKey, key, { contextLoader: mockDocumentLoader });
43
+ }
44
+ /**
45
+ * The DIDs of the users that this server hosts, which the actor dispatcher
46
+ * looks up by identifier, like an application would in its database.
47
+ */
48
+ const users = {
49
+ alice: did,
50
+ instance: did,
51
+ unsigned: did,
52
+ "wrong-key": did,
53
+ tampered: did,
54
+ https: did,
55
+ tombstone: did,
56
+ "mutated-id": did,
57
+ compatible: did,
58
+ "a b/ç": did
59
+ };
60
+ async function portableActor(ctx, identifier, authority) {
61
+ const keys = await ctx.getActorKeyPairs(identifier);
62
+ return new Person({
63
+ id: ctx.getPortableActorUri(identifier, authority),
64
+ preferredUsername: identifier,
65
+ inbox: ctx.getPortableInboxUri(identifier, authority),
66
+ gateways: [new URL(ORIGIN)],
67
+ publicKeys: keys.map((key) => key.cryptographicKey),
68
+ assertionMethods: keys.map((key) => key.multikey)
69
+ });
70
+ }
71
+ function createTestFederation(options = {}) {
72
+ const federation = createFederation({
73
+ kv: new MemoryKvStore(),
74
+ documentLoaderFactory: () => mockDocumentLoader,
75
+ contextLoaderFactory: () => mockDocumentLoader,
76
+ ...options
77
+ });
78
+ const contexts = [];
79
+ const setters = federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
80
+ contexts.push(ctx);
81
+ const userDid = users[identifier];
82
+ if (userDid == null) return null;
83
+ if (ctx.portableRequest != null && ctx.portableRequest.authority !== userDid) return null;
84
+ switch (identifier) {
85
+ case "https": return new Person({ id: ctx.getActorUri(identifier) });
86
+ case "compatible": {
87
+ const actor = await portableActor(ctx, identifier, userDid);
88
+ return await sign(actor.clone({ id: toCompatibleEf61Id(actor.id, ORIGIN) }));
89
+ }
90
+ case "tombstone": return new Tombstone({ id: ctx.getPortableActorUri(identifier, userDid) });
91
+ case "unsigned": return await portableActor(ctx, identifier, userDid);
92
+ case "wrong-key": return await sign(await portableActor(ctx, identifier, userDid), otherKeyPair.privateKey, otherKeyId);
93
+ case "tampered": return (await sign(await portableActor(ctx, identifier, userDid))).clone({ name: "Tampered" });
94
+ case "mutated-id": {
95
+ const signed = await sign(await portableActor(ctx, "alice", userDid));
96
+ const actor = await Person.fromJsonLd(await signed.toJsonLd({ contextLoader: mockDocumentLoader }), {
97
+ contextLoader: mockDocumentLoader,
98
+ documentLoader: mockDocumentLoader
99
+ });
100
+ actor.id.pathname = "/users/mutated-id";
101
+ return actor;
102
+ }
103
+ }
104
+ return await sign(await portableActor(ctx, identifier, userDid));
105
+ }).setKeyPairsDispatcher(() => [{
106
+ privateKey: rsaPrivateKey2,
107
+ publicKey: rsaPublicKey2.publicKey
108
+ }]).mapPortableActorId((ctx, identifier) => {
109
+ const userDid = users[identifier];
110
+ return userDid == null ? null : ctx.getPortableActorUri(identifier, userDid);
111
+ }).mapHandle((_ctx, username) => username).mapActorAlias("/actor", "instance");
112
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
113
+ return {
114
+ federation,
115
+ setters,
116
+ contexts
117
+ };
118
+ }
119
+ test("Context.getPortableActorUri()", async (t) => {
120
+ const { federation } = createTestFederation();
121
+ const ctx = federation.createContext(new URL(`${ORIGIN}/`), void 0);
122
+ await t.step("builds a portable ID from the actor path", () => {
123
+ const uri = ctx.getPortableActorUri("alice", did);
124
+ assertInstanceOf(uri, URL);
125
+ assertEquals(uri.protocol, "ap+ef61:");
126
+ assertEquals(formatIri(uri), `ap+ef61://${did}/users/alice`);
127
+ assertEquals(uri.href, `ap+ef61://${encodeURIComponent(did)}/users/alice`);
128
+ assertEquals(ctx.getActorUri("alice"), new URL(`${ORIGIN}/users/alice`));
129
+ });
130
+ await t.step("uses the same path as getActorUri()", () => {
131
+ for (const identifier of [
132
+ "alice",
133
+ "instance",
134
+ "a b/ç",
135
+ "100%"
136
+ ]) assertEquals(ctx.getPortableActorUri(identifier, did).pathname, ctx.getActorUri(identifier).pathname, identifier);
137
+ assertEquals(formatIri(ctx.getPortableActorUri("instance", did)), `ap+ef61://${did}/actor`);
138
+ });
139
+ await t.step("is serialized in the canonical form", async () => {
140
+ assertEquals((await new Person({ id: ctx.getPortableActorUri("alice", did) }).toJsonLd({ contextLoader: mockDocumentLoader })).id, `ap+ef61://${did}/users/alice`);
141
+ });
142
+ await t.step("converts to and from compatible identifiers", () => {
143
+ const uri = ctx.getPortableActorUri("a b/ç", did);
144
+ const compatible = toCompatibleEf61Id(uri, ORIGIN);
145
+ assertEquals(compatible.href, gatewayUrl(ctx.getActorUri("a b/ç").pathname));
146
+ assertEquals(fromCompatibleEf61Id(compatible)?.href, uri.href);
147
+ });
148
+ await t.step("requires a bare DID authority", () => {
149
+ for (const authority of [
150
+ `${did}/extra`,
151
+ `${did}?query`,
152
+ `${did}#fragment`,
153
+ "did:key:",
154
+ "https://example.com",
155
+ ""
156
+ ]) assertThrows(() => ctx.getPortableActorUri("alice", authority), TypeError, void 0, authority);
157
+ assertThrows(() => ctx.getPortableActorUri("alice"), TypeError);
158
+ });
159
+ await t.step("requires an authority outside gateway requests", () => {
160
+ const requestCtx = federation.createContext(new Request(`${ORIGIN}/`), void 0);
161
+ assertThrows(() => requestCtx.getPortableActorUri("alice"), TypeError);
162
+ assertEquals(formatIri(requestCtx.getPortableActorUri("alice", did)), `ap+ef61://${did}/users/alice`);
163
+ });
164
+ await t.step("requires an actor dispatcher", () => {
165
+ const emptyCtx = createFederation({ kv: new MemoryKvStore() }).createContext(new URL(`${ORIGIN}/`), void 0);
166
+ assertThrows(() => emptyCtx.getPortableActorUri("alice", did), RouterError);
167
+ });
168
+ });
169
+ test("Context portable ID helpers require base58-btc did:key DIDs", async (t) => {
170
+ const { federation } = createTestFederation();
171
+ federation.setObjectDispatcher(Note, "/notes/{id}", () => null);
172
+ const ctx = federation.createContext(new URL(`${ORIGIN}/`), void 0);
173
+ const helpers = {
174
+ getPortableActorUri: (authority) => ctx.getPortableActorUri("alice", authority),
175
+ getPortableObjectUri: (authority) => ctx.getPortableObjectUri(Note, { id: "1" }, authority),
176
+ getPortableInboxUri: (authority) => ctx.getPortableInboxUri("alice", authority)
177
+ };
178
+ const base64url = `did:key:u${did.slice(9)}`;
179
+ for (const [name, helper] of Object.entries(helpers)) await t.step(name, () => {
180
+ for (const authority of [
181
+ base64url,
182
+ base64url.replace("did:key:", "DID:KEY:"),
183
+ `did:key:%75${did.slice(9)}`,
184
+ "did:key:z",
185
+ "did:key:z0OIl"
186
+ ]) assertThrows(() => helper(authority), TypeError, void 0, authority);
187
+ assertEquals(formatIri(helper(did)).startsWith(`ap+ef61://${did}/`), true);
188
+ assertEquals(formatIri(helper(did.replace("did:key:", "DID:KEY:"))), formatIri(helper(did)));
189
+ assertEquals(formatIri(helper("did:web:example.com")).startsWith("ap+ef61://did:web:example.com/"), true);
190
+ });
191
+ });
192
+ test("RequestContext.getPortableActorUri() in gateway requests", async () => {
193
+ const { federation, contexts } = createTestFederation();
194
+ assertEquals((await federation.fetch(gatewayRequest("/users/alice"), { contextData: void 0 })).status, 200);
195
+ const ctx = contexts[0];
196
+ assertEquals(ctx.portableRequest?.authority, did);
197
+ assertEquals(formatIri(ctx.getPortableActorUri("alice")), `ap+ef61://${did}/users/alice`);
198
+ assertEquals(formatIri(ctx.getPortableActorUri("alice", otherDid)), `ap+ef61://${otherDid}/users/alice`);
199
+ });
200
+ test("Federation.fetch() serves portable actors through the actor dispatcher", async (t) => {
201
+ const { federation, contexts } = createTestFederation();
202
+ await t.step("serves a portable actor", async () => {
203
+ contexts.length = 0;
204
+ const response = await federation.fetch(gatewayRequest("/users/alice"), { contextData: void 0 });
205
+ assertEquals(response.status, 200);
206
+ assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
207
+ const json = await response.json();
208
+ assertEquals(json.id, `ap+ef61://${did}/users/alice`);
209
+ assertEquals(json.inbox, `ap+ef61://${did}/users/alice/inbox`);
210
+ assertEquals((await verifyPortableObjectProof(json, { contextLoader: mockDocumentLoader })).verified, true);
211
+ assertEquals(contexts.length, 1);
212
+ assertEquals(contexts[0].portableRequest?.authority, did);
213
+ assertEquals(formatIri(contexts[0].portableRequest.id), `ap+ef61://${did}/users/alice`);
214
+ });
215
+ await t.step("serves an actor with a compatible identifier", async () => {
216
+ const response = await federation.fetch(gatewayRequest("/users/compatible"), { contextData: void 0 });
217
+ assertEquals(response.status, 200);
218
+ assertEquals((await response.json()).id, gatewayUrl("/users/compatible"));
219
+ });
220
+ await t.step("serves an actor with reserved characters", async () => {
221
+ const ctx = federation.createContext(new URL(`${ORIGIN}/`), void 0);
222
+ assertEquals((await federation.fetch(gatewayRequest(ctx.getActorUri("a b/ç").pathname), { contextData: void 0 })).status, 200);
223
+ });
224
+ await t.step("serves an actor alias", async () => {
225
+ let response = await federation.fetch(gatewayRequest("/actor"), { contextData: void 0 });
226
+ assertEquals(response.status, 200);
227
+ assertEquals((await response.json()).id, `ap+ef61://${did}/actor`);
228
+ response = await federation.fetch(gatewayRequest("/users/instance"), { contextData: void 0 });
229
+ assertEquals(response.status, 404);
230
+ });
231
+ await t.step("responds with 400 for non-base58-btc did:key DIDs", async () => {
232
+ contexts.length = 0;
233
+ const base64url = `did:key:u${did.slice(9)}`;
234
+ for (const method of ["GET", "POST"]) assertEquals((await federation.fetch(gatewayRequest(method === "GET" ? "/users/alice" : "/users/alice/inbox", {
235
+ method,
236
+ authority: base64url,
237
+ ...method === "POST" ? { body: "{}" } : {}
238
+ }), { contextData: void 0 })).status, 400, method);
239
+ assertEquals(contexts.length, 0);
240
+ });
241
+ await t.step("responds to HEAD without a body", async () => {
242
+ const response = await federation.fetch(gatewayRequest("/users/alice", { method: "HEAD" }), { contextData: void 0 });
243
+ assertEquals(response.status, 200);
244
+ assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
245
+ assertEquals(response.body, null);
246
+ });
247
+ await t.step("responds with 406 without a JSON-LD Accept header", async () => {
248
+ let notAcceptable = false;
249
+ assertEquals((await federation.fetch(gatewayRequest("/users/alice", { headers: { Accept: "text/html" } }), {
250
+ contextData: void 0,
251
+ onNotAcceptable() {
252
+ notAcceptable = true;
253
+ return new Response("Not acceptable", { status: 406 });
254
+ }
255
+ })).status, 406);
256
+ assertEquals(notAcceptable, true);
257
+ });
258
+ await t.step("responds with 404 for actors it does not serve", async () => {
259
+ for (const request of [
260
+ gatewayRequest("/users/missing"),
261
+ gatewayRequest("/users/https"),
262
+ gatewayRequest("/users/tombstone"),
263
+ gatewayRequest("/users/alice", { authority: otherDid }),
264
+ gatewayRequest("/users/mutated-id")
265
+ ]) {
266
+ let notFound = false;
267
+ assertEquals((await federation.fetch(request, {
268
+ contextData: void 0,
269
+ onNotFound() {
270
+ notFound = true;
271
+ return new Response("Not found", { status: 404 });
272
+ }
273
+ })).status, 404, request.url);
274
+ assertEquals(notFound, true, request.url);
275
+ }
276
+ });
277
+ await t.step("refuses actors that violate the proof policy", async () => {
278
+ for (const identifier of [
279
+ "unsigned",
280
+ "wrong-key",
281
+ "tampered"
282
+ ]) {
283
+ const response = await federation.fetch(gatewayRequest(`/users/${identifier}`), { contextData: void 0 });
284
+ assertEquals(response.status, 500, identifier);
285
+ assertEquals(await response.text(), "Internal server error.");
286
+ }
287
+ });
288
+ await t.step("keeps ordinary actor requests unchanged", async () => {
289
+ const response = await federation.fetch(new Request(`${ORIGIN}/users/https`, { headers: { Accept: ACCEPT } }), { contextData: void 0 });
290
+ assertEquals(response.status, 200);
291
+ assertEquals((await response.json()).id, `${ORIGIN}/users/https`);
292
+ });
293
+ await t.step("leaves POST requests to the actor path alone", async () => {
294
+ let notFound = false;
295
+ assertEquals((await federation.fetch(gatewayRequest("/users/alice", {
296
+ method: "POST",
297
+ body: "{}"
298
+ }), {
299
+ contextData: void 0,
300
+ onNotFound() {
301
+ notFound = true;
302
+ return new Response("Not found", { status: 404 });
303
+ }
304
+ })).status, 404);
305
+ assertEquals(notFound, true);
306
+ });
307
+ });
308
+ test("Federation.fetch() authorizes portable actor requests", async (t) => {
309
+ const { federation, setters } = createTestFederation();
310
+ const identifiers = [];
311
+ setters.authorize(async (ctx, identifier) => {
312
+ identifiers.push(identifier);
313
+ assertEquals(ctx.portableRequest?.authority, did);
314
+ return (await ctx.getSignedKeyOwner())?.id?.href === "https://example.com/person2";
315
+ });
316
+ await t.step("allows a signed request", async () => {
317
+ const request = await signRequest(gatewayRequest("/users/alice?x=1"), rsaPrivateKey3, rsaPublicKey3.id);
318
+ assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 200);
319
+ assertEquals(identifiers, ["alice"]);
320
+ });
321
+ await t.step("denies an unsigned request", async () => {
322
+ let unauthorized = false;
323
+ assertEquals((await federation.fetch(gatewayRequest("/users/alice"), {
324
+ contextData: void 0,
325
+ onUnauthorized() {
326
+ unauthorized = true;
327
+ return new Response("Unauthorized", { status: 401 });
328
+ }
329
+ })).status, 401);
330
+ assertEquals(unauthorized, true);
331
+ });
332
+ await t.step("denies a request whose target was tampered with", async () => {
333
+ const signed = await signRequest(gatewayRequest("/users/alice"), rsaPrivateKey3, rsaPublicKey3.id);
334
+ assertEquals((await federation.fetch(new Request(gatewayUrl("/actor"), { headers: signed.headers }), { contextData: void 0 })).status, 401);
335
+ });
336
+ });
337
+ test("Federation.fetch() serves portable actors found through WebFinger", async () => {
338
+ const { federation } = createTestFederation();
339
+ const webFinger = await federation.fetch(new Request(`${ORIGIN}/.well-known/webfinger?resource=acct:alice@example.com`), { contextData: void 0 });
340
+ assertEquals(webFinger.status, 200);
341
+ const jrd = await webFinger.json();
342
+ assertEquals(jrd.subject, "acct:alice@example.com");
343
+ assertEquals(jrd.links?.[0], {
344
+ rel: "self",
345
+ href: gatewayUrl("/users/alice"),
346
+ type: "application/activity+json"
347
+ });
348
+ const actor = await lookupObject(jrd.links[0].href, {
349
+ documentLoader: federationLoader(federation),
350
+ contextLoader: mockDocumentLoader,
351
+ verifyPortableObject: verifyPortableObjectProof
352
+ });
353
+ assertInstanceOf(actor, Person);
354
+ assertEquals(formatIri(actor.id), `ap+ef61://${did}/users/alice`);
355
+ });
356
+ function federationLoader(federation) {
357
+ return async (url) => {
358
+ const response = await federation.fetch(new Request(url.replace(/#.*$/, ""), { headers: { Accept: ACCEPT } }), { contextData: void 0 });
359
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
360
+ return {
361
+ contextUrl: null,
362
+ documentUrl: url,
363
+ document: await response.json()
364
+ };
365
+ };
366
+ }
367
+ test("Gateway keys of portable actors dereference to the actor dispatcher", async (t) => {
368
+ for (const [label, options] of [["default origin", {}], ["canonical origin", { origin: ORIGIN }]]) await t.step(label, async () => {
369
+ const { federation } = createTestFederation(options);
370
+ const ctx = federation.createContext(new URL("origin" in options ? "https://internal.example/" : `${ORIGIN}/`), void 0);
371
+ for (const identifier of ["alice", "instance"]) {
372
+ const [keyPair] = await ctx.getActorKeyPairs(identifier);
373
+ assertEquals(keyPair.keyId.href, `${toCompatibleEf61Id(ctx.getPortableActorUri(identifier, did), ORIGIN).href}#main-key`);
374
+ const key = await verifyRequest(await signRequest(new Request("https://recipient.example/inbox", {
375
+ method: "POST",
376
+ body: "{}",
377
+ headers: { "Content-Type": "application/activity+json" }
378
+ }), keyPair.privateKey, keyPair.keyId), {
379
+ documentLoader: federationLoader(federation),
380
+ contextLoader: mockDocumentLoader
381
+ });
382
+ assert(key != null, identifier);
383
+ assertEquals(key.id?.href, keyPair.keyId.href);
384
+ assertEquals(key.ownerId?.href, ctx.getPortableActorUri(identifier, did).href);
385
+ }
386
+ });
387
+ });
388
+ test("Federation.fetch() records metrics for portable actor requests", async () => {
389
+ const [meterProvider, recorder] = createTestMeterProvider();
390
+ const { federation } = createTestFederation({ meterProvider });
391
+ let response = await federation.fetch(gatewayRequest("/users/alice"), { contextData: void 0 });
392
+ assertEquals(response.status, 200);
393
+ let counts = recorder.getMeasurements("fedify.http.server.request.count");
394
+ assertEquals(counts.length, 1);
395
+ assertEquals(counts[0].attributes["fedify.endpoint"], "actor");
396
+ const template = counts[0].attributes["fedify.route.template"];
397
+ assertEquals(template, "/.well-known/apgateway/{did}/users/{identifier}");
398
+ response = await federation.fetch(gatewayRequest("/users/alice", { headers: { Accept: "text/html" } }), { contextData: void 0 });
399
+ assertEquals(response.status, 406);
400
+ counts = recorder.getMeasurements("fedify.http.server.request.count");
401
+ assertEquals(counts.length, 2);
402
+ assertEquals(counts[1].attributes["fedify.endpoint"], "not_acceptable");
403
+ });
404
+ test("Federation.fetch() warns about recursive actor dispatches in gateway requests", async () => {
405
+ const records = [];
406
+ await reset();
407
+ try {
408
+ await configure({
409
+ sinks: { buffer: (record) => records.push(record) },
410
+ filters: {},
411
+ loggers: [{
412
+ category: [
413
+ "fedify",
414
+ "federation",
415
+ "actor"
416
+ ],
417
+ sinks: ["buffer"]
418
+ }, {
419
+ category: ["logtape", "meta"],
420
+ sinks: [],
421
+ lowestLevel: "warning"
422
+ }]
423
+ });
424
+ const { federation } = createTestFederation();
425
+ let response = await federation.fetch(gatewayRequest("/users/alice"), { contextData: void 0 });
426
+ assertEquals(response.status, 200);
427
+ const isRecursionWarning = (record) => record.level === "warning" && record.rawMessage.toString().includes("infinite loop");
428
+ assertFalse(records.some(isRecursionWarning));
429
+ const recursive = createFederation({
430
+ kv: new MemoryKvStore(),
431
+ documentLoaderFactory: () => mockDocumentLoader,
432
+ contextLoaderFactory: () => mockDocumentLoader
433
+ });
434
+ recursive.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
435
+ if (identifier === "alice") await ctx.getActor("bob");
436
+ return null;
437
+ });
438
+ response = await recursive.fetch(gatewayRequest("/users/alice"), { contextData: void 0 });
439
+ assertEquals(response.status, 404);
440
+ assert(records.some(isRecursionWarning));
441
+ } finally {
442
+ await reset();
443
+ }
444
+ });
445
+ //#endregion
446
+ 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-DlZhxdFT.mjs";
5
+ import { r as createRequestContext, t as createInboxContext } from "../context-CoNRxGYp.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-B00wWHzc.mjs";
11
- import { l as signRequest } from "../http-DsgawTd8.mjs";
10
+ import { p as signJsonLd } from "../ld-wiJbqJHa.mjs";
11
+ import { l as signRequest } from "../http-BU-8fUrz.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-Br1_d-Ga.mjs";
13
+ import { f as handleInbox, n as FederationImpl, o as createFederation } from "../middleware-FnITXhtz.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";
@@ -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-B00wWHzc.mjs";
12
- import { i as signObject } from "../proof-ChjBwnep.mjs";
13
- import { f as verifyRequestDetailed, l as signRequest } from "../http-DsgawTd8.mjs";
11
+ import { p as signJsonLd } from "../ld-wiJbqJHa.mjs";
12
+ import { i as signObject } from "../proof-BE4T5zF6.mjs";
13
+ import { f as verifyRequestDetailed, l as signRequest } from "../http-BU-8fUrz.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-Br1_d-Ga.mjs";
15
+ import { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-FnITXhtz.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";
@@ -1407,6 +1407,27 @@ test("Context.getPortableInboxUri()", async (t) => {
1407
1407
  assertEquals(formatIri(ctx.getPortableInboxUri("alice", did)), `ap+ef61://${did}/users/alice/inbox`);
1408
1408
  });
1409
1409
  });
1410
+ test("Federation.fetch() serves and delivers to one portable actor dispatcher", async () => {
1411
+ const { federation, received } = setup({ actor: async (ctx, identifier) => {
1412
+ if (identifier !== "alice") return null;
1413
+ if (ctx.portableRequest != null && ctx.portableRequest.authority !== did) return null;
1414
+ return await signObject(new Person({
1415
+ id: ctx.getPortableActorUri(identifier, did),
1416
+ inbox: ctx.getPortableInboxUri(identifier, did),
1417
+ gateways: [new URL(LOCAL)]
1418
+ }), ed25519PrivateKey, keyId, { contextLoader: mockDocumentLoader });
1419
+ } });
1420
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
1421
+ assertEquals(received.length, 1);
1422
+ assertEquals(received[0].recipient, "alice");
1423
+ const get = (authority) => federation.fetch(new Request(inboxUrl(authority, "/users/alice"), { headers: { Accept: "application/activity+json" } }), { contextData: void 0 });
1424
+ const actor = await get(did);
1425
+ assertEquals(actor.status, 200);
1426
+ const json = await actor.json();
1427
+ assertEquals(json.id, `ap+ef61://${did}/users/alice`);
1428
+ assertEquals(json.inbox, `ap+ef61://${did}/users/alice/inbox`);
1429
+ assertEquals((await get(otherDid)).status, 404);
1430
+ });
1410
1431
  test("Federation.fetch() keeps ordinary inbox deliveries unchanged", async () => {
1411
1432
  const queue = new RecordingQueue();
1412
1433
  const { federation, received } = setup({ queue });
@@ -8,10 +8,10 @@ import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
9
  import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
10
10
  import { t as assert } from "../assert-D1Eafmd_.mjs";
11
- import { i as signObject, s as verifyPortableObjectProof } from "../proof-ChjBwnep.mjs";
12
- import { l as signRequest } from "../http-DsgawTd8.mjs";
11
+ import { i as signObject, s as verifyPortableObjectProof } from "../proof-BE4T5zF6.mjs";
12
+ import { l as signRequest } from "../http-BU-8fUrz.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-Br1_d-Ga.mjs";
14
+ import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-FnITXhtz.mjs";
15
15
  import { configure, reset } from "@logtape/logtape";
16
16
  import { Collection, Create, Note, Object as Object$1, Person, Tombstone, lookupObject } from "@fedify/vocab";
17
17
  import { mockDocumentLoader, test } from "@fedify/fixture";
@@ -10,10 +10,10 @@ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
10
10
  import "../std__assert-DvMDRPes.mjs";
11
11
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
12
12
  import { t as assert } from "../assert-D1Eafmd_.mjs";
13
- import { d as verifyRequest } from "../http-DsgawTd8.mjs";
13
+ import { d as verifyRequest } from "../http-BU-8fUrz.mjs";
14
14
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
15
- import { t as doesActorOwnKey } from "../owner-BJPDRDRq.mjs";
16
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-yFX0E-ye.mjs";
15
+ import { t as doesActorOwnKey } from "../owner-BJp47LxN.mjs";
16
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-BFITuKHw.mjs";
17
17
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
19
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
@@ -3,8 +3,8 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-dbCShlCb.mjs";
6
- import "../../testing-CO_cEvQc.mjs";
7
- import { o as createFederation } from "../../middleware-Br1_d-Ga.mjs";
6
+ import "../../testing-BWq1dCzx.mjs";
7
+ import { o as createFederation } from "../../middleware-FnITXhtz.mjs";
8
8
  import { n as ParallelMessageQueue } from "../../mq-Dm1Wmtm-.mjs";
9
9
  import { configure, reset } from "@logtape/logtape";
10
10
  import { delay } from "es-toolkit";
@@ -3,9 +3,9 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-dbCShlCb.mjs";
6
- import "../../testing-CO_cEvQc.mjs";
7
- import { o as createFederation } from "../../middleware-Br1_d-Ga.mjs";
8
- import { r as createFederationBuilder } from "../../builder-Bno54o_-.mjs";
6
+ import "../../testing-BWq1dCzx.mjs";
7
+ import { o as createFederation } from "../../middleware-FnITXhtz.mjs";
8
+ import { r as createFederationBuilder } from "../../builder-CHht75pX.mjs";
9
9
  import { t as TaskCodec } from "../../codec-C6rTrpIp.mjs";
10
10
  import { t as InProcessMessageQueue } from "../../mq-Dm1Wmtm-.mjs";
11
11
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
5
5
  import "../std__assert-DvMDRPes.mjs";
6
6
  import { t as assert } from "../assert-D1Eafmd_.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-Dl9x-Qw4.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-CyDDbE4i.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  //#region src/federation/temporal.test.ts
10
10
  test("hasMalformedKnownTemporalLiteral() detects expanded proof timestamps", async () => {
@@ -1,12 +1,12 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { r as createRequestContext } from "../context-DlZhxdFT.mjs";
4
+ import { r as createRequestContext } from "../context-CoNRxGYp.mjs";
5
5
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
- import { o as createFederation, s as handleWebFinger } from "../middleware-Br1_d-Ga.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-FnITXhtz.mjs";
10
10
  import { Image, Link, Person, Tombstone } from "@fedify/vocab";
11
11
  import { createTestMeterProvider, test } from "@fedify/fixture";
12
12
  import { parseIri } from "@fedify/vocab-runtime";