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

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 (92) hide show
  1. package/dist/{builder-BRlYEsrV.mjs → builder-BCXywsZF.mjs} +4 -4
  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-CHGINiJg.mjs → compound-proof-Fxd45Oy5.mjs} +3 -3
  6. package/dist/{context-Cqx9M_zO.d.cts → context-SMNOiiY6.d.cts} +1 -1
  7. package/dist/{context-B4F4kGJq.d.ts → context-tOiJfBoD.d.ts} +1 -1
  8. package/dist/{deno-CC4RKEEp.mjs → deno-BZrnW0Qz.mjs} +1 -1
  9. package/dist/{docloader-D3_m98fe.mjs → docloader-3BHaS20Z.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +67 -1
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +7 -7
  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.mjs +52 -5
  25. package/dist/federation/portable-gateway-keys.test.mjs +24 -7
  26. package/dist/federation/portable-inbox.test.mjs +4 -4
  27. package/dist/federation/portable.test.mjs +127 -30
  28. package/dist/federation/send.test.mjs +3 -3
  29. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  30. package/dist/federation/tasks/tasks.test.mjs +2 -2
  31. package/dist/federation/temporal.test.mjs +1 -1
  32. package/dist/federation/webfinger.test.mjs +1 -1
  33. package/dist/{federation-dqo6ZzgT.cjs → federation-Ml6NrgRV.cjs} +98 -18
  34. package/dist/{http-B7wEqv49.cjs → http-4bmgw-mA.cjs} +140 -55
  35. package/dist/{http-CHEYA5fH.mjs → http-B9BQDNIy.mjs} +6 -12
  36. package/dist/{http-CaUBCHYJ.js → http-BETASGl0.js} +140 -55
  37. package/dist/{http-xlG-jRdB.d.cts → http-BrQgkJay.d.ts} +12 -7
  38. package/dist/{http-n2v6DPxu.d.ts → http-D76Py0M4.d.cts} +12 -7
  39. package/dist/{key-B8u1vdvF.mjs → key-PyYvun0F.mjs} +107 -16
  40. package/dist/{keycache-BUDHB5rY.mjs → keycache-BInaECGj.mjs} +68 -0
  41. package/dist/{kv-cache-ibncHN1D.js → kv-cache-C4mv2ZFQ.js} +1 -1
  42. package/dist/{kv-cache-CTdvf_Iq.mjs → kv-cache-CPqUxRL3.mjs} +1 -1
  43. package/dist/{kv-cache-0pYMPrkL.cjs → kv-cache-D2Bz6t_c.cjs} +1 -1
  44. package/dist/{ld-BTMJHEGV.mjs → ld-u4JOD4iO.mjs} +4 -7
  45. package/dist/{metrics-CF1TEvdS.mjs → metrics-DCLoLvLz.mjs} +1 -1
  46. package/dist/{middleware-DCMTJT3_.mjs → middleware-B7bmtONC.mjs} +1 -1
  47. package/dist/{middleware-B-rZ7oE-.mjs → middleware-CgOqRkK-.mjs} +49 -37
  48. package/dist/{middleware-Dl-Y_ZlC.js → middleware-DWlAGlyP.js} +106 -26
  49. package/dist/{mod-4s_XgF17.d.ts → mod-COWgSo2v.d.ts} +1 -1
  50. package/dist/{mod-Coacostr.d.ts → mod-CTh7ijx5.d.ts} +1 -1
  51. package/dist/{mod-xGFCHqq_.d.cts → mod-Cr79cQlc.d.cts} +1 -1
  52. package/dist/{mod-CVZrXSdm.d.ts → mod-Cwdt_CWU.d.ts} +1 -1
  53. package/dist/{mod-C8LieBzF.d.cts → mod-DMdGFMEl.d.cts} +1 -1
  54. package/dist/{mod-BuFmStNT.d.cts → mod-DnElMslb.d.cts} +1 -1
  55. package/dist/mod.cjs +5 -5
  56. package/dist/mod.d.cts +5 -5
  57. package/dist/mod.d.ts +5 -5
  58. package/dist/mod.js +5 -5
  59. package/dist/nodeinfo/handler.test.mjs +1 -1
  60. package/dist/{owner-CY--JSqO.mjs → owner-CPFM-sqo.mjs} +4 -4
  61. package/dist/{owner-CGnHPl4k.cjs → owner-DkA0Lz7v.cjs} +1 -1
  62. package/dist/{owner-M8dOKEl6.js → owner-wNinn7RK.js} +1 -1
  63. package/dist/{portable-key-C0-ghkgM.mjs → portable-key-DyylakVV.mjs} +5 -4
  64. package/dist/{portable-key-id-Bva5iT3W.mjs → portable-key-id-T7lecIbt.mjs} +3 -2
  65. package/dist/{proof-DDw0i9b1.mjs → proof-BCOg7Njv.mjs} +32 -31
  66. package/dist/{send-CU3sHGPt.mjs → send-C8eOPat3.mjs} +3 -3
  67. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  68. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  69. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  70. package/dist/sig/compound-proof.test.mjs +2 -2
  71. package/dist/sig/http.test.mjs +2 -2
  72. package/dist/sig/key.test.mjs +1 -1
  73. package/dist/sig/ld.test.mjs +2 -2
  74. package/dist/sig/mod.cjs +3 -3
  75. package/dist/sig/mod.d.cts +2 -2
  76. package/dist/sig/mod.d.ts +2 -2
  77. package/dist/sig/mod.js +3 -3
  78. package/dist/sig/owner.test.mjs +1 -1
  79. package/dist/sig/portable-collection.test.mjs +2 -2
  80. package/dist/sig/portable-key.test.mjs +301 -23
  81. package/dist/sig/proof.test.mjs +3 -3
  82. package/dist/{sig-fycq7sOr.js → sig-C1xOA4ml.js} +2 -2
  83. package/dist/{sig-C2CORtvg.cjs → sig-Cb9VSeiF.cjs} +2 -2
  84. package/dist/{temporal-NHiSpGcW.mjs → temporal-vjHOhBpy.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/utils/docloader.test.mjs +2 -2
  87. package/dist/utils/kv-cache.test.mjs +1 -1
  88. package/dist/utils/mod.cjs +1 -1
  89. package/dist/utils/mod.d.cts +1 -1
  90. package/dist/utils/mod.d.ts +1 -1
  91. package/dist/utils/mod.js +1 -1
  92. package/package.json +7 -7
@@ -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 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-DDw0i9b1.mjs";
12
- import { l as signRequest } from "../http-CHEYA5fH.mjs";
11
+ import { i as signObject, s as verifyPortableObjectProof } from "../proof-BCOg7Njv.mjs";
12
+ import { l as signRequest } from "../http-B9BQDNIy.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-B-rZ7oE-.mjs";
14
+ import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-CgOqRkK-.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";
@@ -220,18 +220,6 @@ test("Federation.fetch() applies the FEP-ef61 proof policy to portable objects",
220
220
  inbox: parseIri(`ap+ef61://${did}/objects/unsigned-actor/inbox`),
221
221
  outbox: parseIri(`ap+ef61://${did}/objects/unsigned-actor/outbox`),
222
222
  gateways: [new URL("https://example.com/")]
223
- })),
224
- "tombstone": () => sign(new Tombstone({
225
- id: parseIri(`ap+ef61://${did}/objects/tombstone`),
226
- formerType: Note
227
- })),
228
- "unsigned-tombstone": () => Promise.resolve(new Tombstone({
229
- id: parseIri(`ap+ef61://${did}/objects/unsigned-tombstone`),
230
- formerType: Note
231
- })),
232
- "mismatched-tombstone": () => sign(new Tombstone({
233
- id: parseIri(`ap+ef61://${did}/objects/other`),
234
- formerType: Note
235
223
  }))
236
224
  };
237
225
  federation.setObjectDispatcher(Object$1, "/objects/{id}", (_ctx, { id }) => objects[id]?.() ?? null);
@@ -246,10 +234,7 @@ test("Federation.fetch() applies the FEP-ef61 proof policy to portable objects",
246
234
  "activity": 200,
247
235
  "actor": 200,
248
236
  "mutated-id": 404,
249
- "unsigned-actor": 500,
250
- "tombstone": 200,
251
- "unsigned-tombstone": 500,
252
- "mismatched-tombstone": 404
237
+ "unsigned-actor": 500
253
238
  })) await t.step(`${id} → ${status}`, async () => {
254
239
  const response = await federation.fetch(gatewayRequest(`/objects/${id}`), { contextData: void 0 });
255
240
  assertEquals(response.status, status);
@@ -260,17 +245,129 @@ test("Federation.fetch() applies the FEP-ef61 proof policy to portable objects",
260
245
  assertEquals(response.status, 500);
261
246
  assertEquals(response.body, null);
262
247
  });
263
- await t.step("serves a signed tombstone with its body", async () => {
264
- const response = await federation.fetch(gatewayRequest("/objects/tombstone"), { contextData: void 0 });
265
- assertEquals(response.status, 200);
266
- const body = await response.json();
267
- assertEquals(body.type, "Tombstone");
268
- assertEquals(body.id, `ap+ef61://${did}/objects/tombstone`);
248
+ });
249
+ test("Federation.fetch() serves tombstones of portable objects", async (t) => {
250
+ const federation = createTestFederation();
251
+ const compatible = (path) => new URL(`https://example.com/.well-known/apgateway/${did}${path}`);
252
+ const tombstoneFromJsonLd = (path, properties) => Tombstone.fromJsonLd({
253
+ "@context": "https://www.w3.org/ns/activitystreams",
254
+ type: "Tombstone",
255
+ id: `ap+ef61://${did}${path}`,
256
+ ...properties
257
+ }, {
258
+ contextLoader: mockDocumentLoader,
259
+ documentLoader: mockDocumentLoader
269
260
  });
270
- await t.step("responds to HEAD for a tombstone without a body", async () => {
271
- const response = await federation.fetch(gatewayRequest("/objects/tombstone", { method: "HEAD" }), { contextData: void 0 });
272
- assertEquals(response.status, 200);
273
- assertEquals(response.body, null);
261
+ const tombstones = {
262
+ "signed": () => sign(new Tombstone({
263
+ id: parseIri(`ap+ef61://${did}/notes/signed`),
264
+ formerType: Note,
265
+ deleted: Temporal.Instant.from("2026-01-01T00:00:00Z")
266
+ })),
267
+ "compatible": () => sign(new Tombstone({
268
+ id: compatible("/notes/compatible"),
269
+ formerType: Note
270
+ })),
271
+ "signed-collection": async () => sign(await tombstoneFromJsonLd("/notes/signed-collection", { totalItems: 0 })),
272
+ "unsigned": () => Promise.resolve(new Tombstone({
273
+ id: parseIri(`ap+ef61://${did}/notes/unsigned`),
274
+ formerType: Note
275
+ })),
276
+ "wrong-did": () => sign(new Tombstone({ id: parseIri(`ap+ef61://${did}/notes/wrong-did`) }), otherKeyPair.privateKey, otherKeyId),
277
+ "tampered": async () => (await sign(new Tombstone({ id: parseIri(`ap+ef61://${did}/notes/tampered`) }))).clone({ formerType: Note }),
278
+ "unsigned-collection": () => tombstoneFromJsonLd("/notes/unsigned-collection", { totalItems: 0 }),
279
+ "link": () => tombstoneFromJsonLd("/notes/link", { href: "https://example.com/" }),
280
+ "mismatched": () => sign(new Tombstone({ id: parseIri(`ap+ef61://${did}/notes/other`) })),
281
+ "mutated-id": async () => {
282
+ const signed = await sign(new Tombstone({ id: parseIri(`ap+ef61://${did}/notes/original`) }));
283
+ const tombstone = await Tombstone.fromJsonLd(await signed.toJsonLd({ contextLoader: mockDocumentLoader }), {
284
+ contextLoader: mockDocumentLoader,
285
+ documentLoader: mockDocumentLoader
286
+ });
287
+ tombstone.id.pathname = tombstone.id.pathname.replace(/original$/, "mutated-id");
288
+ return tombstone;
289
+ }
290
+ };
291
+ federation.setObjectDispatcher(Note, "/notes/{id}", (_ctx, { id }) => tombstones[id]?.() ?? null).authorize((ctx) => ctx.request.headers.get("X-Deny") == null);
292
+ for (const [id, reason] of globalThis.Object.entries({
293
+ "unsigned": "missingProof",
294
+ "wrong-did": "verificationMethodMismatch",
295
+ "tampered": "invalidProof",
296
+ "unsigned-collection": "unsecuredCollection",
297
+ "link": "unsupportedObjectType"
298
+ })) {
299
+ const result = await verifyPortableObjectProof(await (await tombstones[id]()).toJsonLd({ contextLoader: mockDocumentLoader }), { contextLoader: mockDocumentLoader });
300
+ assert(!result.verified, id);
301
+ assertEquals(result.reason.type, reason, id);
302
+ }
303
+ for (const [id, status] of globalThis.Object.entries({
304
+ "signed": 410,
305
+ "compatible": 410,
306
+ "signed-collection": 410,
307
+ "unsigned": 404,
308
+ "wrong-did": 500,
309
+ "tampered": 500,
310
+ "unsigned-collection": 500,
311
+ "link": 500,
312
+ "mismatched": 404,
313
+ "mutated-id": 404
314
+ })) await t.step(`${id} → ${status}`, async () => {
315
+ let notFound = false;
316
+ const response = await federation.fetch(gatewayRequest(`/notes/${id}`), {
317
+ contextData: void 0,
318
+ onNotFound() {
319
+ notFound = true;
320
+ return new Response("Not found", { status: 404 });
321
+ }
322
+ });
323
+ assertEquals(response.status, status);
324
+ assertEquals(notFound, status === 404);
325
+ if (status === 410) {
326
+ assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
327
+ assertEquals(response.headers.get("Vary"), "Accept");
328
+ const body = await response.json();
329
+ assertEquals(body.type, "Tombstone");
330
+ assert((await verifyPortableObjectProof(body, { contextLoader: mockDocumentLoader })).verified);
331
+ } else if (status === 500) assertEquals(await response.text(), "Internal server error.");
332
+ });
333
+ await t.step("serves the tombstone as the dispatcher returns it", async () => {
334
+ let response = await federation.fetch(gatewayRequest("/notes/signed"), { contextData: void 0 });
335
+ let body = await response.json();
336
+ assertEquals(body.id, `ap+ef61://${did}/notes/signed`);
337
+ assertEquals(body.formerType, "as:Note");
338
+ assertEquals(body.deleted, "2026-01-01T00:00:00Z");
339
+ response = await federation.fetch(gatewayRequest("/notes/compatible"), { contextData: void 0 });
340
+ body = await response.json();
341
+ assertEquals(body.id, compatible("/notes/compatible").href);
342
+ });
343
+ await t.step("responds to HEAD without a body", async () => {
344
+ for (const [id, status] of [["signed", 410], ["unsigned", 404]]) {
345
+ const response = await federation.fetch(gatewayRequest(`/notes/${id}`, { method: "HEAD" }), { contextData: void 0 });
346
+ assertEquals(response.status, status, id);
347
+ assertEquals(response.body, null, id);
348
+ if (status === 410) {
349
+ assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
350
+ assertEquals(response.headers.get("Vary"), "Accept");
351
+ }
352
+ }
353
+ });
354
+ await t.step("authorizes before serving a tombstone", async () => {
355
+ for (const id of ["signed", "unsigned"]) {
356
+ let unauthorized = false;
357
+ assertEquals((await federation.fetch(gatewayRequest(`/notes/${id}`, { headers: { "X-Deny": "1" } }), {
358
+ contextData: void 0,
359
+ onUnauthorized() {
360
+ unauthorized = true;
361
+ return new Response("Unauthorized", { status: 401 });
362
+ }
363
+ })).status, 401, id);
364
+ assertEquals(unauthorized, true, id);
365
+ }
366
+ });
367
+ await t.step("does not warn about tombstones with compatible IDs", async () => {
368
+ assertEquals((await captureWarnings(async () => {
369
+ assertEquals((await federation.fetch(gatewayRequest("/notes/compatible"), { contextData: void 0 })).status, 410);
370
+ })).filter((record) => record.category[2] === "object"), []);
274
371
  });
275
372
  });
276
373
  test("Federation.fetch() authorizes portable object requests", async (t) => {
@@ -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-CHEYA5fH.mjs";
13
+ import { d as verifyRequest } from "../http-B9BQDNIy.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-CY--JSqO.mjs";
16
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-CU3sHGPt.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";
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";
@@ -4,7 +4,7 @@ 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
6
  import "../../testing-BWq1dCzx.mjs";
7
- import { o as createFederation } from "../../middleware-B-rZ7oE-.mjs";
7
+ import { o as createFederation } from "../../middleware-CgOqRkK-.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";
@@ -4,8 +4,8 @@ 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
6
  import "../../testing-BWq1dCzx.mjs";
7
- import { o as createFederation } from "../../middleware-B-rZ7oE-.mjs";
8
- import { r as createFederationBuilder } from "../../builder-BRlYEsrV.mjs";
7
+ import { o as createFederation } from "../../middleware-CgOqRkK-.mjs";
8
+ import { r as createFederationBuilder } from "../../builder-BCXywsZF.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-NHiSpGcW.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-vjHOhBpy.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 () => {
@@ -6,7 +6,7 @@ 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-B-rZ7oE-.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-CgOqRkK-.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";
@@ -1,10 +1,10 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-B7wEqv49.cjs");
4
+ const require_http = require("./http-4bmgw-mA.cjs");
5
5
  const require_types = require("./types-Be2zbyeG.cjs");
6
- const require_owner = require("./owner-CGnHPl4k.cjs");
7
- const require_kv_cache = require("./kv-cache-0pYMPrkL.cjs");
6
+ const require_owner = require("./owner-DkA0Lz7v.cjs");
7
+ const require_kv_cache = require("./kv-cache-D2Bz6t_c.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
@@ -750,7 +750,7 @@ function warnMismatchedTombstoneId(context, cls, values, tombstone) {
750
750
  });
751
751
  return;
752
752
  }
753
- if (tombstone.id.protocol === "ap:" || tombstone.id.protocol === "ap+ef61:") return;
753
+ if (require_http.isPortableId(tombstone.id)) return;
754
754
  const expected = context.getObjectUri(cls, values);
755
755
  if (tombstone.id.href !== expected.href) logger.warn("Object dispatcher for {class} returned a tombstone with an id property {tombstoneId} that does not match the object URI {objectUri}. Set the property with Context.getObjectUri().", {
756
756
  class: cls.name,
@@ -2236,6 +2236,11 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
2236
2236
  //#region src/federation/keycache.ts
2237
2237
  const Temporal$9 = require_http.Temporal;
2238
2238
  const KEY_CACHE_GENERATION = "2";
2239
+ const COMPATIBLE_KEY_SEGMENT = "__compatible";
2240
+ const MAX_COMPATIBLE_KEY_TTL = Temporal$9.Duration.from({ hours: 1 });
2241
+ function isCompatibleKeyEntry(value) {
2242
+ return value != null && typeof value === "object" && "key" in value && "expires" in value && typeof value.expires === "number";
2243
+ }
2239
2244
  var KvKeyCache = class {
2240
2245
  kv;
2241
2246
  prefix;
@@ -2298,6 +2303,69 @@ var KvKeyCache = class {
2298
2303
  const serialized = await key.toJsonLd(this.options);
2299
2304
  await this.kv.set(this.#entryKey(keyId), serialized, { ttl: this.keyTtl });
2300
2305
  }
2306
+ /**
2307
+ * Returns the namespace of keys at FEP-ef61 compatible identifiers looked
2308
+ * up for the given purpose. Its entries carry the time they expire, which
2309
+ * is checked when they are read, so that an entry is never used after
2310
+ * the proof that vouched for its key expired, however late the underlying
2311
+ * store evicts it.
2312
+ * @param scope The purpose of the lookups.
2313
+ * @returns The namespace.
2314
+ * @internal
2315
+ */
2316
+ compatibleKeyScope(scope) {
2317
+ const now = this.options.now ?? (() => Temporal$9.Now.instant());
2318
+ const entryKey = (keyId) => [
2319
+ ...this.prefix,
2320
+ COMPATIBLE_KEY_SEGMENT,
2321
+ scope,
2322
+ keyId.href
2323
+ ];
2324
+ const isExpired = (entry) => entry.expires <= now().epochMilliseconds;
2325
+ const keyTtl = Math.min(this.keyTtl.total("millisecond"), MAX_COMPATIBLE_KEY_TTL.total("millisecond"));
2326
+ const unavailableKeyTtl = this.unavailableKeyTtl.total("millisecond");
2327
+ return {
2328
+ get: async (keyId) => {
2329
+ const entry = await this.kv.get(entryKey(keyId));
2330
+ if (entry === void 0) return void 0;
2331
+ if (!isCompatibleKeyEntry(entry) || isExpired(entry)) {
2332
+ await this.kv.delete(entryKey(keyId));
2333
+ return;
2334
+ }
2335
+ if (entry.key === null) return null;
2336
+ let key;
2337
+ try {
2338
+ key = await _fedify_vocab.CryptographicKey.fromJsonLd(entry.key, this.options);
2339
+ } catch {
2340
+ try {
2341
+ key = await _fedify_vocab.Multikey.fromJsonLd(entry.key, this.options);
2342
+ } catch {
2343
+ await this.kv.delete(entryKey(keyId));
2344
+ return;
2345
+ }
2346
+ }
2347
+ return isExpired(entry) ? void 0 : key;
2348
+ },
2349
+ set: async (keyId, key, options) => {
2350
+ const serialized = key == null ? null : await key.toJsonLd(this.options);
2351
+ const current = now();
2352
+ let expires = current.epochMilliseconds + (key == null ? unavailableKeyTtl : keyTtl);
2353
+ if (options?.expires != null) expires = Math.min(expires, options.expires.epochMilliseconds);
2354
+ const ttl = expires - current.epochMilliseconds;
2355
+ if (ttl <= 0) {
2356
+ await this.kv.delete(entryKey(keyId));
2357
+ return;
2358
+ }
2359
+ await this.kv.set(entryKey(keyId), {
2360
+ key: serialized,
2361
+ expires
2362
+ }, { ttl: Temporal$9.Duration.from({ milliseconds: ttl }) });
2363
+ },
2364
+ delete: async (keyId) => {
2365
+ await this.kv.delete(entryKey(keyId));
2366
+ }
2367
+ };
2368
+ }
2301
2369
  async getFetchError(keyId) {
2302
2370
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
2303
2371
  if (cached == null || typeof cached !== "object") return void 0;
@@ -3468,6 +3536,11 @@ async function handleObject(request, { values, context, objectDispatcher, author
3468
3536
  * dispatcher, or the actor dispatcher adapted to one. The object is served
3469
3537
  * only if its ID canonically matches the requested portable ID and it
3470
3538
  * satisfies the FEP-ef61 proof policy.
3539
+ *
3540
+ * A {@link Tombstone} is served with `410 Gone` if it has an Object Integrity
3541
+ * Proof made with a key of the DID in its ID. An unsigned tombstone cannot
3542
+ * be served as a portable object, so the response is the same as for
3543
+ * an object that this server does not store, i.e., `404 Not Found`.
3471
3544
  * @template TContextData The context data to pass to the context.
3472
3545
  * @param request The HTTP request.
3473
3546
  * @param parameters The parameters for handling the portable object.
@@ -3515,17 +3588,27 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3515
3588
  logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
3516
3589
  return await onNotFound(request);
3517
3590
  }
3518
- if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
3519
- logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
3520
- portableId: canonicalId,
3521
- reason: result.reason.type
3522
- });
3523
- return portableObjectInternalServerError(request);
3591
+ const tombstone = object instanceof _fedify_vocab.Tombstone;
3592
+ if (!result.verified) {
3593
+ if (tombstone && result.reason.type === "missingProof") {
3594
+ logger.debug("Not serving the tombstone of the portable object {portableId}, as it has no Object Integrity Proof; responding as if this server did not store the object.", { portableId: canonicalId });
3595
+ return await onNotFound(request);
3596
+ }
3597
+ if (tombstone || result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
3598
+ logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, objects, and tombstones need an Object Integrity Proof made with a key of the DID in their ID.", {
3599
+ portableId: canonicalId,
3600
+ reason: result.reason.type
3601
+ });
3602
+ return portableObjectInternalServerError(request);
3603
+ }
3524
3604
  }
3525
- return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
3526
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
3527
- Vary: "Accept"
3528
- } });
3605
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
3606
+ status: tombstone ? 410 : 200,
3607
+ headers: {
3608
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
3609
+ Vary: "Accept"
3610
+ }
3611
+ });
3529
3612
  }
3530
3613
  function isRequestedPortableObject(object, canonicalId) {
3531
3614
  return object.id != null && require_http.getCanonicalPortableId(object.id) === canonicalId;
@@ -8320,10 +8403,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8320
8403
  return await handlePortableObject(request, {
8321
8404
  values: { identifier },
8322
8405
  context,
8323
- objectDispatcher: actorDispatcher == null ? void 0 : async (ctx) => {
8324
- const actor = await actorDispatcher(ctx, identifier);
8325
- return actor instanceof _fedify_vocab.Tombstone ? null : actor;
8326
- },
8406
+ objectDispatcher: actorDispatcher == null ? void 0 : (ctx) => actorDispatcher(ctx, identifier),
8327
8407
  authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
8328
8408
  canonicalId: portable.canonicalId,
8329
8409
  onUnauthorized,