@fedify/fedify 2.4.0-dev.2122 → 2.4.0-dev.2129

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 (98) hide show
  1. package/dist/{builder-BCXywsZF.mjs → builder-C0bxZDL0.mjs} +3 -3
  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-Fxd45Oy5.mjs → compound-proof-D9Kljk_7.mjs} +3 -3
  6. package/dist/{context-tOiJfBoD.d.ts → context-Cz1w5Krc.d.ts} +31 -5
  7. package/dist/{context-CoNRxGYp.mjs → context-DfSVPcJ8.mjs} +6 -3
  8. package/dist/{context-SMNOiiY6.d.cts → context-Dvec_D_h.d.cts} +31 -5
  9. package/dist/{deno-BZrnW0Qz.mjs → deno-_o3lkDlG.mjs} +1 -1
  10. package/dist/{docloader-3BHaS20Z.mjs → docloader-BbS0rO-Y.mjs} +7 -7
  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 +6 -6
  24. package/dist/federation/portable-actor.test.mjs +3 -3
  25. package/dist/federation/portable-chain.test.d.mts +2 -0
  26. package/dist/federation/portable-chain.test.mjs +358 -0
  27. package/dist/federation/portable-gateway-keys.test.mjs +77 -6
  28. package/dist/federation/portable-inbox.test.mjs +9 -4
  29. package/dist/federation/portable.test.mjs +3 -3
  30. package/dist/federation/send.test.mjs +3 -3
  31. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  32. package/dist/federation/tasks/tasks.test.mjs +3 -3
  33. package/dist/federation/temporal.test.mjs +1 -1
  34. package/dist/federation/webfinger.test.mjs +2 -2
  35. package/dist/{federation-Ml6NrgRV.cjs → federation-DRY8KrkB.cjs} +46 -22
  36. package/dist/{http-4bmgw-mA.cjs → http-B1J6E-bT.cjs} +384 -33
  37. package/dist/{http-B9BQDNIy.mjs → http-BpOiZE09.mjs} +18 -12
  38. package/dist/{http-D76Py0M4.d.cts → http-C-x_OLSp.d.ts} +57 -14
  39. package/dist/{http-BETASGl0.js → http-C2M6wcs8.js} +374 -35
  40. package/dist/{http-BrQgkJay.d.ts → http-CjezPjFX.d.cts} +57 -14
  41. package/dist/{key-PyYvun0F.mjs → key-DolJjJab.mjs} +103 -16
  42. package/dist/{kv-cache-CPqUxRL3.mjs → kv-cache-Bq2cah0c.mjs} +1 -1
  43. package/dist/{kv-cache-C4mv2ZFQ.js → kv-cache-D7TiyMxv.js} +6 -6
  44. package/dist/{kv-cache-D2Bz6t_c.cjs → kv-cache-D9nRbG2Q.cjs} +6 -6
  45. package/dist/{ld-u4JOD4iO.mjs → ld-Dwc176V4.mjs} +3 -3
  46. package/dist/{metrics-DCLoLvLz.mjs → metrics-CDaHfBXY.mjs} +1 -1
  47. package/dist/{middleware-B7bmtONC.mjs → middleware-CmB1FD8A.mjs} +1 -1
  48. package/dist/{middleware-DWlAGlyP.js → middleware-CylX-pvI.js} +52 -28
  49. package/dist/{middleware-CgOqRkK-.mjs → middleware-DN3MdTDg.mjs} +64 -39
  50. package/dist/{mod-CTh7ijx5.d.ts → mod-Bz9tFXI2.d.ts} +14 -3
  51. package/dist/{mod-Cwdt_CWU.d.ts → mod-CZetqT5h.d.ts} +1 -1
  52. package/dist/{mod-Cr79cQlc.d.cts → mod-D4g9ffEn.d.cts} +1 -1
  53. package/dist/{mod-DnElMslb.d.cts → mod-DGOIxEfb.d.cts} +1 -1
  54. package/dist/{mod-DMdGFMEl.d.cts → mod-S--JxYjo.d.cts} +14 -3
  55. package/dist/{mod-COWgSo2v.d.ts → mod-owwElvpw.d.ts} +1 -1
  56. package/dist/mod.cjs +8 -8
  57. package/dist/mod.d.cts +5 -5
  58. package/dist/mod.d.ts +5 -5
  59. package/dist/mod.js +5 -5
  60. package/dist/nodeinfo/handler.test.mjs +2 -2
  61. package/dist/{owner-CPFM-sqo.mjs → owner-BNHhwjNu.mjs} +37 -4
  62. package/dist/{sig-C1xOA4ml.js → portable-collection-CRpTUHCY.mjs} +5 -4
  63. package/dist/portable-collection-CgglAI9B.js +656 -0
  64. package/dist/{sig-Cb9VSeiF.cjs → portable-collection-PTGD0q70.cjs} +253 -2
  65. package/dist/portable-key-DqX2emHQ.mjs +395 -0
  66. package/dist/{portable-key-id-T7lecIbt.mjs → portable-key-id-Bg259c4Z.mjs} +69 -2
  67. package/dist/{proof-BCOg7Njv.mjs → proof-B4_0E-KU.mjs} +5 -5
  68. package/dist/{send-C8eOPat3.mjs → send-Dpu5qbP5.mjs} +3 -3
  69. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  70. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  71. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  72. package/dist/sig/compound-proof.test.mjs +2 -2
  73. package/dist/sig/http.test.mjs +2 -2
  74. package/dist/sig/key.test.mjs +1 -1
  75. package/dist/sig/ld.test.mjs +2 -2
  76. package/dist/sig/mod.cjs +5 -6
  77. package/dist/sig/mod.d.cts +2 -2
  78. package/dist/sig/mod.d.ts +2 -2
  79. package/dist/sig/mod.js +2 -3
  80. package/dist/sig/owner.test.mjs +1 -1
  81. package/dist/sig/portable-collection.test.mjs +20 -431
  82. package/dist/sig/portable-key.test.mjs +479 -5
  83. package/dist/sig/proof.test.mjs +3 -3
  84. package/dist/{temporal-vjHOhBpy.mjs → temporal-78JVR3GK.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/testing/mod.d.mts +30 -4
  87. package/dist/testing/mod.mjs +2 -2
  88. package/dist/{testing-BWq1dCzx.mjs → testing-otQhu4aD.mjs} +1 -1
  89. package/dist/utils/docloader.test.mjs +81 -2
  90. package/dist/utils/kv-cache.test.mjs +1 -1
  91. package/dist/utils/mod.cjs +1 -1
  92. package/dist/utils/mod.d.cts +1 -1
  93. package/dist/utils/mod.d.ts +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +7 -7
  96. package/dist/owner-DkA0Lz7v.cjs +0 -225
  97. package/dist/owner-wNinn7RK.js +0 -215
  98. package/dist/portable-key-DyylakVV.mjs +0 -214
@@ -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-DolJjJab.mjs";
11
+ import { o as verifyObject, s as verifyPortableObjectProof } from "../proof-B4_0E-KU.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-DN3MdTDg.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-CoNRxGYp.mjs";
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-u4JOD4iO.mjs";
11
- import { l as signRequest } from "../http-B9BQDNIy.mjs";
10
+ import { p as signJsonLd } from "../ld-Dwc176V4.mjs";
11
+ import { l as signRequest } from "../http-BpOiZE09.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-CgOqRkK-.mjs";
13
+ import { f as handleInbox, n as FederationImpl, o as createFederation } from "../middleware-DN3MdTDg.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";
@@ -214,6 +214,24 @@ test("Context.sendActivity() never signs documents with gateway keys", async ()
214
214
  assertEquals(bodies[0].proof, void 0);
215
215
  assertEquals(bodies[0].signature, void 0);
216
216
  });
217
+ test("Context.sendActivity() never signs documents with keys at ap: URIs", async () => {
218
+ const ctx = createTestFederation().createContext(new URL(gateway));
219
+ const keys = [{
220
+ keyId: parseIri(`${actorId.href}#main-key`),
221
+ privateKey: rsaPrivateKey2
222
+ }, {
223
+ keyId: parseIri(`${actorId.href}#key-2`),
224
+ privateKey: ed25519GatewayKeyPair.privateKey
225
+ }];
226
+ const activity = new Create({
227
+ id: new URL("https://example.com/activities/1"),
228
+ actor: new URL("https://example.com/users/bob"),
229
+ object: new URL("https://example.com/notes/1")
230
+ });
231
+ const { bodies } = await capture(() => ctx.sendActivity(keys, recipient, activity));
232
+ assertEquals(bodies[0].proof, void 0);
233
+ assertEquals(bodies[0].signature, void 0);
234
+ });
217
235
  test("Context.getDocumentLoader() signs requests with gateway keys for portable actors", async () => {
218
236
  const documentLoader = await createTestFederation(() => actorId).createContext(new URL(gateway)).getDocumentLoader({ identifier: "alice" });
219
237
  let keyId;
@@ -255,14 +273,14 @@ function createLoader(responses) {
255
273
  });
256
274
  };
257
275
  }
258
- async function deliver(body, gateways, { kv = new MemoryKvStore(), httpSignature = true, actor, documentLoader } = {}) {
276
+ async function deliver(body, gateways, { kv = new MemoryKvStore(), httpSignature = true, actor, documentLoader, keyId = gatewayKeyId } = {}) {
259
277
  documentLoader ??= createLoader({ [compatibleActorId(gateway)]: actor ?? await actorDocument(gateways) });
260
278
  const unsigned = new Request("https://local.example/inbox", {
261
279
  method: "POST",
262
280
  body: JSON.stringify(body),
263
281
  headers: { "Content-Type": "application/activity+json" }
264
282
  });
265
- const request = httpSignature ? await signRequest(unsigned, rsaPrivateKey2, gatewayKeyId) : unsigned;
283
+ const request = httpSignature ? await signRequest(unsigned, rsaPrivateKey2, keyId) : unsigned;
266
284
  const context = createRequestContext({
267
285
  federation: createFederation({ kv: new MemoryKvStore() }),
268
286
  request,
@@ -332,6 +350,59 @@ test("handleInbox() requires proofs of portable actors despite gateway signature
332
350
  dispatched: 1
333
351
  });
334
352
  });
353
+ const apKeyId = parseIri(`${actorId.href}?@gateway=${encodeURIComponent(gateway)}#main-key`);
354
+ async function apActorDocument() {
355
+ const json = await new Person({
356
+ id: actorId,
357
+ inbox: parseIri(`${actorId.href}/inbox`),
358
+ gateways: [new URL(gateway)],
359
+ publicKey: new CryptographicKey({
360
+ id: parseIri(`${actorId.href}#main-key`),
361
+ owner: actorId,
362
+ publicKey: rsaPublicKey2.publicKey
363
+ })
364
+ }).toJsonLd({
365
+ format: "compact",
366
+ contextLoader: mockDocumentLoader
367
+ });
368
+ json["@context"] = [...json["@context"], "https://w3id.org/security/data-integrity/v1"];
369
+ return await sign(json);
370
+ }
371
+ test("handleInbox() requires proofs of portable actors despite signatures with keys at ap: key IDs", async () => {
372
+ const fetched = [];
373
+ const inner = createLoader({ [compatibleActorId(gateway)]: await apActorDocument() });
374
+ const documentLoader = (url, options) => {
375
+ fetched.push(url);
376
+ return inner(url, options);
377
+ };
378
+ assertEquals(await deliver(await createJson("unsigned"), [gateway], {
379
+ documentLoader,
380
+ keyId: apKeyId
381
+ }), {
382
+ status: 401,
383
+ dispatched: 0
384
+ });
385
+ assertEquals(fetched.some((url) => url.startsWith(compatibleActorId(gateway))), true);
386
+ assertEquals(await deliver(await sign(await createJson("signed")), [gateway], {
387
+ documentLoader,
388
+ keyId: apKeyId
389
+ }), {
390
+ status: 202,
391
+ dispatched: 1
392
+ });
393
+ });
394
+ test("RequestContext.getSignedKeyOwner() resolves keys at ap: key IDs", async () => {
395
+ const documentLoader = createLoader({ [compatibleActorId(gateway)]: await apActorDocument() });
396
+ const federation = new FederationImpl({
397
+ kv: new MemoryKvStore(),
398
+ contextLoaderFactory: () => mockDocumentLoader,
399
+ documentLoaderFactory: () => documentLoader
400
+ });
401
+ const request = await signRequest(new Request("https://local.example/notes/1", { headers: { Accept: "application/activity+json" } }), rsaPrivateKey2, apKeyId);
402
+ const ctx = federation.createContext(request, void 0);
403
+ assertEquals((await ctx.getSignedKey())?.id?.href, apKeyId.href);
404
+ assertEquals((await ctx.getSignedKeyOwner())?.id?.href, actorId.href);
405
+ });
335
406
  test("handleInbox() fetches an unreachable gateway key once", async () => {
336
407
  const fetched = [];
337
408
  const documentLoader = (url) => {
@@ -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-u4JOD4iO.mjs";
12
- import { i as signObject } from "../proof-BCOg7Njv.mjs";
13
- import { f as verifyRequestDetailed, l as signRequest } from "../http-B9BQDNIy.mjs";
11
+ import { p as signJsonLd } from "../ld-Dwc176V4.mjs";
12
+ import { i as signObject } from "../proof-B4_0E-KU.mjs";
13
+ import { f as verifyRequestDetailed, l as signRequest } from "../http-BpOiZE09.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-CgOqRkK-.mjs";
15
+ import { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-DN3MdTDg.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";
@@ -1089,6 +1089,11 @@ test("Federation.fetch() forwards portable inbox deliveries whose sending gatewa
1089
1089
  sign: { keyId: bobKeyId },
1090
1090
  fetches: false
1091
1091
  },
1092
+ {
1093
+ name: "key at an ap: key ID",
1094
+ sign: { keyId: parseIri(`ap+ef61://${did}/users/alice?@gateway=${encodeURIComponent(GATEWAY2)}#main-key`) },
1095
+ fetches: false
1096
+ },
1092
1097
  {
1093
1098
  name: "content-digest not covered",
1094
1099
  sign: {
@@ -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-BCOg7Njv.mjs";
12
- import { l as signRequest } from "../http-B9BQDNIy.mjs";
11
+ import { i as signObject, s as verifyPortableObjectProof } from "../proof-B4_0E-KU.mjs";
12
+ import { l as signRequest } from "../http-BpOiZE09.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-CgOqRkK-.mjs";
14
+ import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-DN3MdTDg.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-B9BQDNIy.mjs";
13
+ import { d as verifyRequest } from "../http-BpOiZE09.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-CPFM-sqo.mjs";
16
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-C8eOPat3.mjs";
15
+ import { t as doesActorOwnKey } from "../owner-BNHhwjNu.mjs";
16
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-Dpu5qbP5.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-BWq1dCzx.mjs";
7
- import { o as createFederation } from "../../middleware-CgOqRkK-.mjs";
6
+ import "../../testing-otQhu4aD.mjs";
7
+ import { o as createFederation } from "../../middleware-DN3MdTDg.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-BWq1dCzx.mjs";
7
- import { o as createFederation } from "../../middleware-CgOqRkK-.mjs";
8
- import { r as createFederationBuilder } from "../../builder-BCXywsZF.mjs";
6
+ import "../../testing-otQhu4aD.mjs";
7
+ import { o as createFederation } from "../../middleware-DN3MdTDg.mjs";
8
+ import { r as createFederationBuilder } from "../../builder-C0bxZDL0.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-vjHOhBpy.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-78JVR3GK.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-CoNRxGYp.mjs";
4
+ import { r as createRequestContext } from "../context-DfSVPcJ8.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-CgOqRkK-.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-DN3MdTDg.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";