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

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 (93) hide show
  1. package/dist/{builder-CHht75pX.mjs → builder-CZcBWrBd.mjs} +29 -5
  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-DKKH_Nr1.mjs → compound-proof-CDuP9MIX.mjs} +3 -3
  6. package/dist/{context-mDTxTw-S.d.cts → context-SMNOiiY6.d.cts} +93 -11
  7. package/dist/{context-CfJE_oQu.d.ts → context-tOiJfBoD.d.ts} +93 -11
  8. package/dist/{deno-CkjiYK_Z.mjs → deno-CCABdw7r.mjs} +1 -1
  9. package/dist/{docloader-C1hAJnWc.mjs → docloader-Bby5g2iu.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 +118 -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 +184 -7
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  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 +3 -3
  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 +31 -4
  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-C1dL6SiT.cjs → federation-DYr384xz.cjs} +116 -15
  34. package/dist/{http-Cala2Grs.cjs → http-B4mQvNcv.cjs} +140 -55
  35. package/dist/{http-BU-8fUrz.mjs → http-BcLnr2do.mjs} +6 -12
  36. package/dist/{http-xlG-jRdB.d.cts → http-BrQgkJay.d.ts} +12 -7
  37. package/dist/{http-tTlTeFa0.js → http-Ce6_Lf7k.js} +140 -55
  38. package/dist/{http-n2v6DPxu.d.ts → http-D76Py0M4.d.cts} +12 -7
  39. package/dist/{key-I8gLBkcf.mjs → key-CkNxztNV.mjs} +107 -16
  40. package/dist/{keycache-BUDHB5rY.mjs → keycache-BInaECGj.mjs} +68 -0
  41. package/dist/{kv-cache-Bl5EBFrU.cjs → kv-cache-Dqp_E-1g.cjs} +1 -1
  42. package/dist/{kv-cache-CxQu_cd4.js → kv-cache-DwzXzchj.js} +1 -1
  43. package/dist/{kv-cache-DJFoGRWQ.mjs → kv-cache-iFaRIcIL.mjs} +1 -1
  44. package/dist/{ld-wiJbqJHa.mjs → ld-CnT0f1nU.mjs} +4 -7
  45. package/dist/{metrics-BCAa5-kE.mjs → metrics-em_7lwhG.mjs} +1 -1
  46. package/dist/{middleware-FnITXhtz.mjs → middleware-BpwKvOpU.mjs} +26 -17
  47. package/dist/{middleware-B6IVTVL0.js → middleware-DhIBngkj.js} +108 -7
  48. package/dist/{middleware-6p2dYEVA.mjs → middleware-U9IuzvDO.mjs} +1 -1
  49. package/dist/{mod-BYvPz_T0.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-B3OzOobN.d.cts → mod-DnElMslb.d.cts} +1 -1
  55. package/dist/mod.cjs +5 -5
  56. package/dist/mod.d.cts +7 -7
  57. package/dist/mod.d.ts +7 -7
  58. package/dist/mod.js +5 -5
  59. package/dist/nodeinfo/handler.test.mjs +1 -1
  60. package/dist/{owner-BJp47LxN.mjs → owner-9LiHmOv1.mjs} +4 -4
  61. package/dist/{owner-DCt8W_Cv.cjs → owner-BtoMCv6Z.cjs} +1 -1
  62. package/dist/{owner-j5yrbXS1.js → owner-Cb3ytrlq.js} +1 -1
  63. package/dist/{portable-key-B5oKE_wZ.mjs → portable-key-9ck61lI0.mjs} +5 -4
  64. package/dist/{portable-key-id-Bva5iT3W.mjs → portable-key-id-T7lecIbt.mjs} +3 -2
  65. package/dist/{proof-BE4T5zF6.mjs → proof-xdPYGPaA.mjs} +32 -31
  66. package/dist/{send-BFITuKHw.mjs → send-Bf8rSWch.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-CRTRoouH.cjs → sig-Bj3fHnv0.cjs} +2 -2
  83. package/dist/{sig-CJHdGk9w.js → sig-DN9pOglI.js} +2 -2
  84. package/dist/{temporal-CyDDbE4i.mjs → temporal-tAtwv6vX.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/testing/mod.d.mts +91 -9
  87. package/dist/utils/docloader.test.mjs +2 -2
  88. package/dist/utils/kv-cache.test.mjs +1 -1
  89. package/dist/utils/mod.cjs +1 -1
  90. package/dist/utils/mod.d.cts +1 -1
  91. package/dist/utils/mod.d.ts +1 -1
  92. package/dist/utils/mod.js +1 -1
  93. package/package.json +7 -7
@@ -1,11 +1,15 @@
1
- import "temporal-polyfill";
1
+ import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { r as fetchKey } from "../key-I8gLBkcf.mjs";
5
- import { d as verifyRequest, l as signRequest } from "../http-BU-8fUrz.mjs";
6
- import { c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
7
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-BJp47LxN.mjs";
8
- import { Create, CryptographicKey, Multikey, Person } from "@fedify/vocab";
4
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
+ import { i as fetchKey } from "../key-CkNxztNV.mjs";
6
+ import { h as verifyJsonLd, p as signJsonLd } from "../ld-CnT0f1nU.mjs";
7
+ import { n as createProof, u as verifyProof } from "../proof-xdPYGPaA.mjs";
8
+ import { d as verifyRequest, f as verifyRequestDetailed, l as signRequest } from "../http-BcLnr2do.mjs";
9
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
10
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-9LiHmOv1.mjs";
11
+ import { t as KvKeyCache } from "../keycache-BInaECGj.mjs";
12
+ import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
9
13
  import { test } from "@fedify/fixture";
10
14
  import { FetchError, encodeMultibase, exportDidKey, exportSpki, parseIri, preloadedContexts } from "@fedify/vocab-runtime";
11
15
  import serialize from "json-canon";
@@ -46,14 +50,15 @@ function createLoader(responses) {
46
50
  };
47
51
  return Object.assign(loader, { fetched });
48
52
  }
49
- async function sign(document, privateKey = ed25519PrivateKey, verificationMethod = didKeyId) {
53
+ async function sign(document, privateKey = ed25519PrivateKey, verificationMethod = didKeyId, proofOptions = {}) {
50
54
  const proofConfig = {
51
55
  "@context": document["@context"],
52
56
  type: "DataIntegrityProof",
53
57
  cryptosuite: "eddsa-jcs-2022",
54
58
  verificationMethod,
55
59
  proofPurpose: "assertionMethod",
56
- created: "2023-02-24T23:36:38Z"
60
+ created: "2023-02-24T23:36:38Z",
61
+ ...proofOptions
57
62
  };
58
63
  const encoder = new TextEncoder();
59
64
  const proofDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)));
@@ -333,32 +338,305 @@ test("keys at ordinary URLs cannot belong to portable actors", async () => {
333
338
  keyCache
334
339
  })).key, null);
335
340
  });
336
- test("gateway keys are neither cached nor accepted outside HTTP Signatures", async () => {
341
+ const specs = ["draft-cavage-http-signatures-12", "rfc9421"];
342
+ function fetchKeyOf(cls, options) {
343
+ return cls === CryptographicKey ? fetchKey(keyId, CryptographicKey, options) : fetchKey(keyId, Multikey, options);
344
+ }
345
+ function fetchCount(loader, url = keyId) {
346
+ const document = url.href.replace(/#.*$/, "");
347
+ return loader.fetched.filter((u) => u.replace(/#.*$/, "") === document).length;
348
+ }
349
+ test("verifyRequest() caches failures to fetch compatible key IDs", async () => {
350
+ const unreachable = {
351
+ "404 Not Found": createLoader({}),
352
+ "a network error": (url) => Promise.reject(/* @__PURE__ */ new TypeError(`${url}`))
353
+ };
354
+ for (const spec of specs) for (const [name, inner] of Object.entries(unreachable)) {
355
+ const fetched = [];
356
+ const documentLoader = Object.assign((url) => {
357
+ fetched.push(url);
358
+ return inner(url);
359
+ }, { fetched });
360
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
361
+ const options = {
362
+ documentLoader,
363
+ contextLoader,
364
+ keyCache,
365
+ spec
366
+ };
367
+ for (let i = 0; i < 3; i++) {
368
+ const result = await verifyRequestDetailed(await signedRequest(spec), options);
369
+ ok(!result.verified, `${spec}, ${name}`);
370
+ strictEqual(result.reason.type, "keyFetchError", `${spec}, ${name}`);
371
+ }
372
+ strictEqual(fetchCount(documentLoader), 1, `${spec}, ${name}`);
373
+ for (const cls of [CryptographicKey, Multikey]) {
374
+ const result = await fetchKeyOf(cls, options);
375
+ strictEqual(result.key, null);
376
+ ok(result.cached, `${spec}, ${name}, ${cls.name}`);
377
+ }
378
+ strictEqual(fetchCount(documentLoader), 1, `${spec}, ${name}`);
379
+ }
380
+ });
381
+ test("verifyRequest() caches gateway keys only for HTTP Signatures", async () => {
382
+ for (const spec of specs) {
383
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
384
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
385
+ const options = {
386
+ documentLoader,
387
+ contextLoader,
388
+ keyCache,
389
+ spec
390
+ };
391
+ for (let i = 0; i < 3; i++) strictEqual((await verifyRequest(await signedRequest(spec), options))?.ownerId?.href, parseIri(actorId).href, spec);
392
+ strictEqual(fetchCount(documentLoader), 1, spec);
393
+ const document = {
394
+ "@context": "https://www.w3.org/ns/activitystreams",
395
+ type: "Create",
396
+ id: "https://gw1.example/activities/1",
397
+ actor: compatibleId(gw1)
398
+ };
399
+ const { signature } = await signJsonLd(document, rsaPrivateKey2, keyId, { contextLoader });
400
+ ok(!await verifyJsonLd({
401
+ ...document,
402
+ signature
403
+ }, options), spec);
404
+ const note = new Note({
405
+ id: new URL("https://gw1.example/notes/1"),
406
+ attribution: new URL(compatibleId(gw1)),
407
+ content: "Hello"
408
+ });
409
+ const proof = await createProof(note, ed25519PrivateKey, keyId, { contextLoader });
410
+ strictEqual(await verifyProof(await note.toJsonLd({
411
+ format: "compact",
412
+ contextLoader
413
+ }), proof, options), null, spec);
414
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null, spec);
415
+ const fetched = fetchCount(documentLoader);
416
+ ok(await verifyRequest(await signedRequest(spec), options) != null, spec);
417
+ strictEqual(fetchCount(documentLoader), fetched, spec);
418
+ }
419
+ });
420
+ test("verifyRequest() is not rejected by other purposes' negative entries", async () => {
421
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
422
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
423
+ const options = {
424
+ documentLoader,
425
+ contextLoader,
426
+ keyCache
427
+ };
428
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null);
429
+ ok(await verifyRequest(await signedRequest(), options) != null);
430
+ const fetched = fetchCount(documentLoader);
431
+ for (const cls of [CryptographicKey, Multikey]) {
432
+ const result = await fetchKeyOf(cls, options);
433
+ strictEqual(result.key, null);
434
+ ok(result.cached);
435
+ }
436
+ strictEqual(fetchCount(documentLoader), fetched);
437
+ });
438
+ test("fetchKey() does not share negative entries of compatible key IDs across key classes", async () => {
439
+ const ownerId = new URL("https://gw1.example/users/bob");
440
+ const multikey = new Multikey({
441
+ id: keyId,
442
+ controller: ownerId,
443
+ publicKey: ed25519PublicKey.publicKey
444
+ });
445
+ const documentLoader = createLoader({
446
+ [compatibleId(gw1)]: await multikey.toJsonLd({ contextLoader }),
447
+ [ownerId.href]: await new Person({
448
+ id: ownerId,
449
+ assertionMethods: [multikey]
450
+ }).toJsonLd({ contextLoader })
451
+ });
452
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
453
+ const options = {
454
+ documentLoader,
455
+ contextLoader,
456
+ keyCache
457
+ };
458
+ strictEqual((await fetchKey(keyId, CryptographicKey, options)).key, null);
459
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
460
+ const result = await fetchKey(keyId, Multikey, options);
461
+ strictEqual(result.key?.id?.href, keyId.href);
462
+ ok(!result.cached);
463
+ const cached = await fetchKey(keyId, Multikey, options);
464
+ strictEqual(cached.key?.id?.href, keyId.href);
465
+ ok(cached.cached);
466
+ const other = await fetchKey(keyId, CryptographicKey, options);
467
+ strictEqual(other.key, null);
468
+ ok(other.cached);
469
+ });
470
+ test("verifyRequest() caches rejected portable actor documents", async () => {
471
+ for (const spec of specs) {
472
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await actorJson() });
473
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
474
+ const options = {
475
+ documentLoader,
476
+ contextLoader,
477
+ keyCache,
478
+ spec
479
+ };
480
+ for (let i = 0; i < 3; i++) {
481
+ const result = await verifyRequestDetailed(await signedRequest(spec), options);
482
+ ok(!result.verified, spec);
483
+ strictEqual(result.reason.type, "invalidSignature", spec);
484
+ }
485
+ strictEqual(fetchCount(documentLoader), 1, spec);
486
+ }
487
+ });
488
+ test("verifyRequest() refetches a cached gateway key that fails to verify", async () => {
489
+ for (const spec of specs) {
490
+ const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
491
+ const documentLoader = createLoader(responses);
492
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
493
+ const options = {
494
+ documentLoader,
495
+ contextLoader,
496
+ keyCache,
497
+ spec
498
+ };
499
+ ok(await verifyRequest(await signedRequest(spec), options) != null);
500
+ responses[compatibleId(gw1)] = await sign(await actorJson({
501
+ publicKey: rsaPublicKey3.publicKey,
502
+ assertionMethod: false
503
+ }));
504
+ const rotated = async () => await signRequest(new Request("https://recipient.example/inbox", {
505
+ method: "POST",
506
+ body: "{}",
507
+ headers: { "Content-Type": "application/activity+json" }
508
+ }), rsaPrivateKey3, keyId, { spec });
509
+ ok(await verifyRequest(await rotated(), options) != null, spec);
510
+ strictEqual(fetchCount(documentLoader), 2, spec);
511
+ ok(await verifyRequest(await rotated(), options) != null, spec);
512
+ strictEqual(fetchCount(documentLoader), 2, spec);
513
+ }
514
+ });
515
+ test("verifyRequest() drops a cached gateway key when its refetch fails", async () => {
516
+ for (const spec of specs) {
517
+ const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
518
+ const documentLoader = createLoader(responses);
519
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
520
+ const options = {
521
+ documentLoader,
522
+ contextLoader,
523
+ keyCache,
524
+ spec
525
+ };
526
+ ok(await verifyRequest(await signedRequest(spec), options) != null);
527
+ delete responses[compatibleId(gw1)];
528
+ const result = await verifyRequestDetailed(await signRequest(new Request("https://recipient.example/inbox", {
529
+ method: "POST",
530
+ body: "{}",
531
+ headers: { "Content-Type": "application/activity+json" }
532
+ }), rsaPrivateKey3, keyId, { spec }), options);
533
+ ok(!result.verified, spec);
534
+ strictEqual(result.reason.type, "keyFetchError", spec);
535
+ strictEqual(fetchCount(documentLoader), 2, spec);
536
+ const next = await verifyRequestDetailed(await signedRequest(spec), options);
537
+ ok(!next.verified, spec);
538
+ strictEqual(next.reason.type, "keyFetchError", spec);
539
+ strictEqual(fetchCount(documentLoader), 2, spec);
540
+ }
541
+ });
542
+ test("verifyRequest() does not trust a cached gateway key for longer than its proof", async () => {
543
+ let now = Temporal.Now.instant();
544
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
545
+ const expires = now.add({ minutes: 30 });
546
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson(), ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
547
+ const options = {
548
+ documentLoader,
549
+ contextLoader,
550
+ keyCache
551
+ };
552
+ ok(await verifyRequest(await signedRequest(), options) != null);
553
+ now = expires.subtract({ seconds: 1 });
554
+ ok(await verifyRequest(await signedRequest(), options) != null);
555
+ strictEqual(fetchCount(documentLoader), 1);
556
+ now = expires;
557
+ ok(await verifyRequest(await signedRequest(), options) != null);
558
+ strictEqual(fetchCount(documentLoader), 2);
559
+ });
560
+ test("verifyRequest() looks up a cached gateway key again after an hour", async () => {
561
+ let now = Temporal.Now.instant();
562
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
563
+ const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
564
+ const documentLoader = createLoader(responses);
565
+ const options = {
566
+ documentLoader,
567
+ contextLoader,
568
+ keyCache
569
+ };
570
+ const start = now;
571
+ ok(await verifyRequest(await signedRequest(), options) != null);
572
+ now = start.add({ minutes: 59 });
573
+ ok(await verifyRequest(await signedRequest(), options) != null);
574
+ strictEqual(fetchCount(documentLoader), 1);
575
+ responses[compatibleId(gw1)] = await sign(await actorJson({ gateways: [gw2] }));
576
+ now = start.add({ hours: 1 });
577
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
578
+ strictEqual(fetchCount(documentLoader), 2);
579
+ });
580
+ test("verifyRequest() rejects and caches gateway keys of expired documents", async () => {
581
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson(), ed25519PrivateKey, didKeyId, { expires: "2000-01-01T00:00:00Z" }) });
582
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
583
+ const options = {
584
+ documentLoader,
585
+ contextLoader,
586
+ keyCache
587
+ };
588
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
589
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
590
+ strictEqual(fetchCount(documentLoader), 1);
591
+ });
592
+ test("verifyRequest() takes the expiration of a proof whatever its context", async () => {
593
+ let now = Temporal.Now.instant();
594
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
595
+ const expires = now.add({ minutes: 30 });
596
+ const actor = await actorJson();
597
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign({
598
+ ...actor,
599
+ "@context": [...actor["@context"], { expires: "https://example.com/ns#notExpiration" }]
600
+ }, ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
601
+ const options = {
602
+ documentLoader,
603
+ contextLoader,
604
+ keyCache
605
+ };
606
+ ok(await verifyRequest(await signedRequest(), options) != null);
607
+ now = expires.subtract({ seconds: 1 });
608
+ ok(await verifyRequest(await signedRequest(), options) != null);
609
+ strictEqual(fetchCount(documentLoader), 1);
610
+ now = expires;
611
+ ok(await verifyRequest(await signedRequest(), options) != null);
612
+ strictEqual(fetchCount(documentLoader), 2);
613
+ });
614
+ test("a key cache without namespaces caches only fetch failures of compatible key IDs", async () => {
337
615
  const calls = [];
616
+ const entries = /* @__PURE__ */ new Map();
338
617
  const keyCache = {
339
618
  get(id) {
340
619
  calls.push(`get ${id.href}`);
341
- return Promise.resolve(void 0);
620
+ return Promise.resolve(entries.get(id.href));
342
621
  },
343
- set(id) {
344
- calls.push(`set ${id.href}`);
622
+ set(id, key) {
623
+ calls.push(`set ${id.href} ${key == null ? "null" : "key"}`);
624
+ entries.set(id.href, key);
345
625
  return Promise.resolve();
346
626
  }
347
627
  };
348
- const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
349
- ok(await verifyRequest(await signedRequest(), {
350
- documentLoader,
351
- contextLoader,
352
- keyCache
353
- }) != null);
354
- const fetchOptions = {
355
- documentLoader,
628
+ const options = {
629
+ documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) }),
356
630
  contextLoader,
357
631
  keyCache
358
632
  };
359
- strictEqual((await fetchKey(keyId, CryptographicKey, fetchOptions)).key, null);
360
- strictEqual((await fetchKey(keyId, Multikey, fetchOptions)).key, null);
361
- deepStrictEqual(calls.filter((c) => c.includes(keyId.origin)), []);
633
+ ok(await verifyRequest(await signedRequest(), options) != null);
634
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null);
635
+ ok(await verifyRequest(await signedRequest(), options) != null);
636
+ deepStrictEqual(calls.filter((c) => c.startsWith("set ") && c.includes(keyId.origin)), []);
637
+ const unreachable = new URL(`${compatibleId(gw2)}#main-key`);
638
+ strictEqual((await fetchKey(unreachable, CryptographicKey, options)).key, null);
639
+ deepStrictEqual(calls.filter((c) => c.startsWith("set ") && c.includes(gw2)), [`set ${unreachable.href} null`]);
362
640
  });
363
641
  test("doesActorOwnKey() and getKeyOwner() resolve gateway keys to portable actors", async () => {
364
642
  const options = {
@@ -10,10 +10,10 @@ import "../std__assert-DvMDRPes.mjs";
10
10
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
11
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
12
12
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-4IJqPYos.mjs";
13
- import { i as signObject, n as createProof, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, u as verifyProof } from "../proof-BE4T5zF6.mjs";
14
- import { d as verifyRequest } from "../http-BU-8fUrz.mjs";
13
+ import { i as signObject, n as createProof, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, u as verifyProof } from "../proof-xdPYGPaA.mjs";
14
+ import { d as verifyRequest } from "../http-BcLnr2do.mjs";
15
15
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
16
- import { t as getAuthenticatedDocumentLoader } from "../docloader-C1hAJnWc.mjs";
16
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-Bby5g2iu.mjs";
17
17
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place, Translation } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
19
  import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
@@ -1,6 +1,6 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
- const require_http = require("./http-Cala2Grs.cjs");
3
- require("./owner-DCt8W_Cv.cjs");
2
+ const require_http = require("./http-B4mQvNcv.cjs");
3
+ require("./owner-BtoMCv6Z.cjs");
4
4
  let _logtape_logtape = require("@logtape/logtape");
5
5
  let _fedify_vocab = require("@fedify/vocab");
6
6
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { _ as verifyPortableObjectProofWithRoot, bt as name, h as verifyPortableObjectProof, xt as version } from "./http-tTlTeFa0.js";
4
- import "./owner-j5yrbXS1.js";
3
+ import { _ as verifyPortableObjectProofWithRoot, bt as name, h as verifyPortableObjectProof, xt as version } from "./http-Ce6_Lf7k.js";
4
+ import "./owner-Cb3ytrlq.js";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { CollectionPage, Object as Object$1, isActor } from "@fedify/vocab";
7
7
  import { trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as getNormalizationContextLoader } from "./ld-wiJbqJHa.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-CnT0f1nU.mjs";
5
5
  import jsonld from "@fedify/vocab-runtime/jsonld";
6
6
  //#region src/federation/temporal.ts
7
7
  function isPlainObject(value) {
@@ -6,7 +6,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
7
7
  import { n as baseOptions, o as stringSchema, t as MockQueue } from "../tasks-dbCShlCb.mjs";
8
8
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
9
- import { o as createFederation } from "../middleware-FnITXhtz.mjs";
9
+ import { o as createFederation } from "../middleware-BpwKvOpU.mjs";
10
10
  //#region src/testing/kv.test.ts
11
11
  test("ManualClockKvStore keeps an entry at its exact expiration instant", async () => {
12
12
  const kv = new ManualClockKvStore();
@@ -387,14 +387,18 @@ type ActorAliasMapper<TContextData> = (context: RequestContext<TContextData>, re
387
387
  username: string;
388
388
  } | null>;
389
389
  /**
390
- * A callback that dispatches an object.
390
+ * A callback that dispatches an object or a {@link Tombstone}.
391
+ *
392
+ * Return a {@link Tombstone} if the object has been deleted; Fedify then
393
+ * responds with `410 Gone` and the serialized tombstone. Return `null` if
394
+ * the object is not found.
391
395
  *
392
396
  * @template TContextData The context data to pass to the {@link Context}.
393
397
  * @template TObject The type of object to dispatch.
394
398
  * @template TParam The parameter names of the requested URL.
395
399
  * @since 0.7.0
396
400
  */
397
- type ObjectDispatcher<TContextData, TObject extends Object$1, TParam extends string> = (context: RequestContext<TContextData>, values: Record<TParam, string>) => TObject | null | Promise<TObject | null>;
401
+ type ObjectDispatcher<TContextData, TObject extends Object$1, TParam extends string> = (context: RequestContext<TContextData>, values: Record<TParam, string>) => TObject | Tombstone | null | Promise<TObject | Tombstone | null>;
398
402
  /**
399
403
  * A callback that dispatches a collection.
400
404
  *
@@ -1525,7 +1529,9 @@ interface Federatable<TContextData> extends TaskRegistry<TContextData> {
1525
1529
  * based on URI Template
1526
1530
  * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path
1527
1531
  * must have one or more variables.
1528
- * @param dispatcher An object dispatcher callback to register.
1532
+ * @param dispatcher An object dispatcher callback to register. It may
1533
+ * return an object, a `Tombstone` if the object has been
1534
+ * deleted, or `null` if the object is not found.
1529
1535
  */
1530
1536
  setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: ConstructorWithTypeId<TObject>, path: `${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}`, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
1531
1537
  /**
@@ -1539,7 +1545,9 @@ interface Federatable<TContextData> extends TaskRegistry<TContextData> {
1539
1545
  * based on URI Template
1540
1546
  * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path
1541
1547
  * must have one or more variables.
1542
- * @param dispatcher An object dispatcher callback to register.
1548
+ * @param dispatcher An object dispatcher callback to register. It may
1549
+ * return an object, a `Tombstone` if the object has been
1550
+ * deleted, or `null` if the object is not found.
1543
1551
  */
1544
1552
  setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: ConstructorWithTypeId<TObject>, path: `${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}`, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
1545
1553
  /**
@@ -1553,7 +1561,9 @@ interface Federatable<TContextData> extends TaskRegistry<TContextData> {
1553
1561
  * based on URI Template
1554
1562
  * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path
1555
1563
  * must have one or more variables.
1556
- * @param dispatcher An object dispatcher callback to register.
1564
+ * @param dispatcher An object dispatcher callback to register. It may
1565
+ * return an object, a `Tombstone` if the object has been
1566
+ * deleted, or `null` if the object is not found.
1557
1567
  */
1558
1568
  setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: ConstructorWithTypeId<TObject>, path: `${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}{${TParam}}${string}`, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
1559
1569
  /**
@@ -1567,7 +1577,9 @@ interface Federatable<TContextData> extends TaskRegistry<TContextData> {
1567
1577
  * based on URI Template
1568
1578
  * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path
1569
1579
  * must have one or more variables.
1570
- * @param dispatcher An object dispatcher callback to register.
1580
+ * @param dispatcher An object dispatcher callback to register. It may
1581
+ * return an object, a `Tombstone` if the object has been
1582
+ * deleted, or `null` if the object is not found.
1571
1583
  */
1572
1584
  setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: ConstructorWithTypeId<TObject>, path: `${string}${Rfc6570Expression<TParam>}${string}${Rfc6570Expression<TParam>}${string}${Rfc6570Expression<TParam>}${string}`, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
1573
1585
  /**
@@ -1581,7 +1593,9 @@ interface Federatable<TContextData> extends TaskRegistry<TContextData> {
1581
1593
  * based on URI Template
1582
1594
  * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path
1583
1595
  * must have one or more variables.
1584
- * @param dispatcher An object dispatcher callback to register.
1596
+ * @param dispatcher An object dispatcher callback to register. It may
1597
+ * return an object, a `Tombstone` if the object has been
1598
+ * deleted, or `null` if the object is not found.
1585
1599
  */
1586
1600
  setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: ConstructorWithTypeId<TObject>, path: `${string}${Rfc6570Expression<TParam>}${string}${Rfc6570Expression<TParam>}${string}`, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
1587
1601
  /**
@@ -1595,7 +1609,9 @@ interface Federatable<TContextData> extends TaskRegistry<TContextData> {
1595
1609
  * based on URI Template
1596
1610
  * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path
1597
1611
  * must have one or more variables.
1598
- * @param dispatcher An object dispatcher callback to register.
1612
+ * @param dispatcher An object dispatcher callback to register. It may
1613
+ * return an object, a `Tombstone` if the object has been
1614
+ * deleted, or `null` if the object is not found.
1599
1615
  */
1600
1616
  setObjectDispatcher<TObject extends Object$1, TParam extends string>(cls: ConstructorWithTypeId<TObject>, path: `${string}${Rfc6570Expression<TParam>}${string}`, dispatcher: ObjectDispatcher<TContextData, TObject, TParam>): ObjectCallbackSetters<TContextData, TObject, TParam>;
1601
1617
  /**
@@ -3357,6 +3373,23 @@ interface GetActorOptions {
3357
3373
  */
3358
3374
  readonly tombstone?: "suppress" | "passthrough";
3359
3375
  }
3376
+ /**
3377
+ * Options for {@link RequestContext.getObject}.
3378
+ * @since 2.4.0
3379
+ */
3380
+ interface GetObjectOptions {
3381
+ /**
3382
+ * Controls how tombstoned objects are returned.
3383
+ *
3384
+ * By default, or if set to `"suppress"`, a {@link Tombstone} is returned as
3385
+ * `null`, unless it is an instance of the requested class, e.g., when
3386
+ * the requested class is `Object` or `Tombstone` itself. In that case,
3387
+ * the tombstone is returned as is, as it is an object of the requested
3388
+ * class. Set this to `"passthrough"` to always receive a {@link Tombstone}
3389
+ * result instead of `null`.
3390
+ */
3391
+ readonly tombstone?: "suppress" | "passthrough";
3392
+ }
3360
3393
  /**
3361
3394
  * A context for a request.
3362
3395
  */
@@ -3522,12 +3555,61 @@ interface RequestContext<TContextData> extends Context<TContextData> {
3522
3555
  * @param cls The class to instantiate.
3523
3556
  * @param values The values to pass to the object dispatcher.
3524
3557
  * @returns The object of the given class with the given values, or `null`
3525
- * if the object is not found.
3558
+ * if the object is not found. Tombstoned objects are suppressed
3559
+ * unless the tombstone is an instance of `cls`.
3526
3560
  * @throws {Error} If no object dispatcher is available for the class.
3527
3561
  * @throws {TypeError} If values are invalid.
3528
3562
  * @since 0.7.0
3529
3563
  */
3530
3564
  getObject<TObject extends Object$1>(cls: ConstructorWithTypeId<TObject>, values: Record<string, string>): Promise<TObject | null>;
3565
+ /**
3566
+ * Gets an object of the given class, or a {@link Tombstone}, with the given
3567
+ * values.
3568
+ * @param cls The class to instantiate.
3569
+ * @param values The values to pass to the object dispatcher.
3570
+ * @param options Options for getting the object. Set `options.tombstone`
3571
+ * to `"passthrough"` to receive tombstoned objects instead
3572
+ * of `null`.
3573
+ * @returns The object of the given class with the given values,
3574
+ * a tombstone, or `null` if the object is not found.
3575
+ * @throws {Error} If no object dispatcher is available for the class.
3576
+ * @throws {TypeError} If values are invalid.
3577
+ * @since 2.4.0
3578
+ */
3579
+ getObject<TObject extends Object$1>(cls: ConstructorWithTypeId<TObject>, values: Record<string, string>, options: GetObjectOptions & {
3580
+ readonly tombstone: "passthrough";
3581
+ }): Promise<TObject | Tombstone | null>;
3582
+ /**
3583
+ * Gets an object of the given class with the given values.
3584
+ * @param cls The class to instantiate.
3585
+ * @param values The values to pass to the object dispatcher.
3586
+ * @param options Options for getting the object.
3587
+ * @returns The object of the given class with the given values, or `null`
3588
+ * if the object is not found. Tombstoned objects are suppressed
3589
+ * unless `options.tombstone` is `"passthrough"` or the tombstone
3590
+ * is an instance of `cls`.
3591
+ * @throws {Error} If no object dispatcher is available for the class.
3592
+ * @throws {TypeError} If values are invalid.
3593
+ * @since 2.4.0
3594
+ */
3595
+ getObject<TObject extends Object$1>(cls: ConstructorWithTypeId<TObject>, values: Record<string, string>, options: GetObjectOptions & {
3596
+ readonly tombstone?: "suppress" | undefined;
3597
+ }): Promise<TObject | null>;
3598
+ /**
3599
+ * Gets an object of the given class, or a {@link Tombstone}, with the given
3600
+ * values.
3601
+ * @param cls The class to instantiate.
3602
+ * @param values The values to pass to the object dispatcher.
3603
+ * @param options Options for getting the object.
3604
+ * @returns The object of the given class with the given values,
3605
+ * a tombstone, or `null` if the object is not found. This broad
3606
+ * overload is used when the caller passes an options value whose
3607
+ * `tombstone` mode is not known statically.
3608
+ * @throws {Error} If no object dispatcher is available for the class.
3609
+ * @throws {TypeError} If values are invalid.
3610
+ * @since 2.4.0
3611
+ */
3612
+ getObject<TObject extends Object$1>(cls: ConstructorWithTypeId<TObject>, values: Record<string, string>, options: GetObjectOptions): Promise<TObject | Tombstone | null>;
3531
3613
  /**
3532
3614
  * Gets the public key of the sender, if any exists and it is verified.
3533
3615
  * Otherwise, `null` is returned.
@@ -5,9 +5,9 @@ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import "../std__assert-DvMDRPes.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
8
- import { d as verifyRequest } from "../http-BU-8fUrz.mjs";
8
+ import { d as verifyRequest } from "../http-BcLnr2do.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-C1hAJnWc.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-Bby5g2iu.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
13
13
  //#region src/utils/docloader.test.ts
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as kvCache, t as MockKvStore } from "../kv-cache-DJFoGRWQ.mjs";
4
+ import { n as kvCache, t as MockKvStore } from "../kv-cache-iFaRIcIL.mjs";
5
5
  import { deepStrictEqual, throws } from "node:assert";
6
6
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
7
7
  import { preloadedContexts } from "@fedify/vocab-runtime";
@@ -1,5 +1,5 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_kv_cache = require("../kv-cache-Bl5EBFrU.cjs");
3
+ const require_kv_cache = require("../kv-cache-Dqp_E-1g.cjs");
4
4
  exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
5
5
  exports.kvCache = require_kv_cache.kvCache;
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-xGFCHqq_.cjs";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Cr79cQlc.cjs";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-CVZrXSdm.js";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Cwdt_CWU.js";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
package/dist/utils/mod.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-CxQu_cd4.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-DwzXzchj.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };