@fedify/fedify 2.3.0-dev.1145 → 2.3.0-dev.1154

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 (68) hide show
  1. package/dist/{builder-ShiR1K6b.mjs → builder-BwSH45lU.mjs} +2 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{context-DI2gRbyN.d.cts → context-CRXCkTM6.d.cts} +48 -6
  6. package/dist/{context-DCtsSHDv.d.ts → context-MgCh7YGu.d.ts} +48 -6
  7. package/dist/{deno-h0TWFuEz.mjs → deno-Aas8ryCk.mjs} +1 -1
  8. package/dist/{docloader-BdDN0Aqx.mjs → docloader-_1lh7Dn1.mjs} +2 -2
  9. package/dist/federation/builder.test.mjs +1 -1
  10. package/dist/federation/handler.test.mjs +1363 -44
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/metrics.test.mjs +1 -1
  13. package/dist/federation/middleware.test.mjs +1667 -163
  14. package/dist/federation/mod.cjs +1 -1
  15. package/dist/federation/mod.d.cts +2 -2
  16. package/dist/federation/mod.d.ts +2 -2
  17. package/dist/federation/mod.js +1 -1
  18. package/dist/federation/retry.test.mjs +1 -1
  19. package/dist/federation/send.test.mjs +8 -8
  20. package/dist/federation/temporal.test.d.mts +2 -0
  21. package/dist/federation/temporal.test.mjs +71 -0
  22. package/dist/federation/webfinger.test.mjs +1 -1
  23. package/dist/{getMachineId-bsd-etIyxDet.mjs → getMachineId-bsd-BY01PL1n.mjs} +1 -1
  24. package/dist/{getMachineId-darwin-D23zTf4g.mjs → getMachineId-darwin-Dr1gkBkp.mjs} +1 -1
  25. package/dist/{getMachineId-win-Dpap6v5i.mjs → getMachineId-win-QEYwcJiy.mjs} +1 -1
  26. package/dist/{http-B2hxA7dO.js → http-CP1Qje2Q.js} +1 -1
  27. package/dist/{http-QzW9IWfs.mjs → http-DUBr4pJL.mjs} +3 -3
  28. package/dist/{http-7kAB7PVx.cjs → http-bAPHYmg8.cjs} +1 -1
  29. package/dist/{key-Dh2OK1XQ.mjs → key-DVh4I9kS.mjs} +2 -2
  30. package/dist/{kv-cache-DCPp-MT0.cjs → kv-cache-2zYOM6Q7.cjs} +1 -1
  31. package/dist/{kv-cache-EZRIPZXD.mjs → kv-cache-6CA6Rx92.mjs} +1 -1
  32. package/dist/{kv-cache-b22dNkjt.js → kv-cache-azKKIdQE.js} +1 -1
  33. package/dist/{ld-eZbar1rr.mjs → ld-D9435Gn1.mjs} +302 -6
  34. package/dist/{metrics-E0hAHtLZ.mjs → metrics-K7CyLZhK.mjs} +1 -1
  35. package/dist/{middleware-mToCR2tG.mjs → middleware-Bn8SYmaa.mjs} +1 -1
  36. package/dist/{middleware-BUl1BH4x.cjs → middleware-Cu8Lw81i.cjs} +429 -99
  37. package/dist/{middleware-CyJDCmNg.mjs → middleware-DZjXGmiF.mjs} +348 -108
  38. package/dist/{middleware-BrGIM_Ra.js → middleware-tbfw9GfY.js} +428 -99
  39. package/dist/{mod-CI9fduEi.d.cts → mod-C7HOzGqH.d.cts} +1 -1
  40. package/dist/{mod-CkRiJHGA.d.ts → mod-CpQHB3Ys.d.ts} +1 -1
  41. package/dist/mod.cjs +4 -4
  42. package/dist/mod.d.cts +2 -2
  43. package/dist/mod.d.ts +2 -2
  44. package/dist/mod.js +4 -4
  45. package/dist/nodeinfo/handler.test.mjs +1 -1
  46. package/dist/{owner-ByO_Fw6U.mjs → owner-Bt9Zdipr.mjs} +2 -2
  47. package/dist/{proof-jVqClF49.cjs → proof-3YeQ4Z5A.cjs} +353 -3
  48. package/dist/{proof-CSo0S8OK.mjs → proof-CfevUec1.mjs} +3 -3
  49. package/dist/{proof-BkRyFchv.js → proof-CqwCBFaT.js} +300 -4
  50. package/dist/{send-jzrTV1FU.mjs → send-CuS5FYIt.mjs} +3 -3
  51. package/dist/sig/http.test.mjs +2 -2
  52. package/dist/sig/key.test.mjs +1 -1
  53. package/dist/sig/ld.test.mjs +558 -2
  54. package/dist/sig/mod.cjs +2 -2
  55. package/dist/sig/mod.js +2 -2
  56. package/dist/sig/owner.test.mjs +1 -1
  57. package/dist/sig/proof.test.mjs +1 -1
  58. package/dist/temporal-BacpfwuJ.mjs +95 -0
  59. package/dist/testing/mod.d.mts +48 -6
  60. package/dist/utils/docloader.test.mjs +2 -2
  61. package/dist/utils/kv-cache.test.mjs +1 -1
  62. package/dist/utils/mod.cjs +1 -1
  63. package/dist/utils/mod.js +1 -1
  64. package/package.json +6 -6
  65. /package/dist/{execAsync-DCBrgFiV.mjs → execAsync-Dxb7rNf3.mjs} +0 -0
  66. /package/dist/{getMachineId-linux-ObI47Hql.mjs → getMachineId-linux-Bbhofx-s.mjs} +0 -0
  67. /package/dist/{getMachineId-unsupported-Ddu-PFeh.mjs → getMachineId-unsupported-dIOte2Ct.mjs} +0 -0
  68. /package/dist/{retry-v_sGLH1d.mjs → retry-_VvV0h9f.mjs} +0 -0
@@ -10,18 +10,18 @@ import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
10
10
  import { t as assertStrictEquals } from "../assert_strict_equals-XEgZAlrj.mjs";
11
11
  import { t as assert } from "../assert-OguE97r2.mjs";
12
12
  import { t as esm_default } from "../esm-BQRw925N.mjs";
13
- import { l as verifyRequest, s as signRequest } from "../http-QzW9IWfs.mjs";
13
+ import { l as verifyRequest, s as signRequest } from "../http-DUBr4pJL.mjs";
14
14
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-C3kae-6B.mjs";
15
- import { t as getAuthenticatedDocumentLoader } from "../docloader-BdDN0Aqx.mjs";
16
- import { a as signJsonLd, o as verifyJsonLd, r as detachSignature } from "../ld-eZbar1rr.mjs";
17
- import { t as doesActorOwnKey } from "../owner-ByO_Fw6U.mjs";
18
- import { i as verifyObject, r as signObject } from "../proof-CSo0S8OK.mjs";
15
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-_1lh7Dn1.mjs";
16
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-D9435Gn1.mjs";
17
+ import { t as doesActorOwnKey } from "../owner-Bt9Zdipr.mjs";
18
+ import { i as verifyObject, r as signObject } from "../proof-CfevUec1.mjs";
19
19
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
20
- import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-CyJDCmNg.mjs";
20
+ import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-DZjXGmiF.mjs";
21
21
  import { configure, reset } from "@logtape/logtape";
22
22
  import { RouterError } from "@fedify/uri-template";
23
23
  import * as vocab from "@fedify/vocab";
24
- import { getTypeId, lookupObject } from "@fedify/vocab";
24
+ import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
25
25
  import { SpanStatusCode } from "@opentelemetry/api";
26
26
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
27
27
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
@@ -1978,6 +1978,54 @@ test("Federation.setOutboxListeners()", async (t) => {
1978
1978
  assertEquals(enqueuedMetrics[0].attributes["fedify.queue.task.attempt"], 0);
1979
1979
  });
1980
1980
  });
1981
+ test("Federation.fetch() preserves original LD-signed payload for InboxContextImpl.activity", async () => {
1982
+ const remoteContextUrl = "https://remote.example/contexts/ext";
1983
+ const sourceContextLoader = async (resource) => {
1984
+ const url = new URL(resource).href;
1985
+ if (url === remoteContextUrl) return {
1986
+ contextUrl: null,
1987
+ documentUrl: url,
1988
+ document: { "@context": { ext: "https://example.com/ext" } }
1989
+ };
1990
+ return await mockDocumentLoader(url);
1991
+ };
1992
+ const federation = createFederation({
1993
+ kv: new MemoryKvStore(),
1994
+ documentLoaderFactory: () => mockDocumentLoader,
1995
+ contextLoaderFactory: () => sourceContextLoader
1996
+ });
1997
+ federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "someone" ? new Person({}) : null);
1998
+ let receivedRaw = null;
1999
+ let receivedTyped = null;
2000
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, (ctx, activity) => {
2001
+ receivedRaw = ctx.activity;
2002
+ receivedTyped = activity;
2003
+ });
2004
+ const signed = await signJsonLd({
2005
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
2006
+ id: "https://example.com/activities/preserve-raw",
2007
+ type: "Create",
2008
+ actor: "https://example.com/person2",
2009
+ ext: "preserve-me",
2010
+ object: {
2011
+ id: "https://example.com/notes/preserve-raw",
2012
+ type: "Note",
2013
+ attributedTo: "https://example.com/person2",
2014
+ content: "Hello, world!"
2015
+ }
2016
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: sourceContextLoader });
2017
+ const response = await federation.fetch(new Request("https://example.com/inbox", {
2018
+ method: "POST",
2019
+ headers: { "Content-Type": "application/activity+json" },
2020
+ body: JSON.stringify(signed)
2021
+ }), { contextData: void 0 });
2022
+ assertEquals([response.status, await response.text()], [202, ""]);
2023
+ assertEquals(receivedRaw, signed);
2024
+ assertNotEquals(receivedRaw, await compactJsonLd(signed, sourceContextLoader));
2025
+ const delivered = receivedTyped;
2026
+ assert(delivered != null);
2027
+ assertEquals(delivered.id?.href, "https://example.com/activities/preserve-raw");
2028
+ });
1981
2029
  test("Federation.setInboxDispatcher()", async (t) => {
1982
2030
  const kv = new MemoryKvStore();
1983
2031
  await t.step("path match", () => {
@@ -2341,147 +2389,1588 @@ test("FederationImpl.processQueuedTask()", async (t) => {
2341
2389
  assertEquals(outboxLifecycle[0].attributes["activitypub.processing.result"], "retried");
2342
2390
  assertEquals(outboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2343
2391
  });
2344
- await t.step("records activitypub.outbox.activity abandoned when retry policy gives up", async () => {
2392
+ await t.step("records activitypub.outbox.activity abandoned when retry policy gives up", async () => {
2393
+ const kv = new MemoryKvStore();
2394
+ const [meterProvider, recorder] = createTestMeterProvider();
2395
+ await new FederationImpl({
2396
+ kv,
2397
+ meterProvider,
2398
+ queue: {
2399
+ enqueue(_message, _options) {
2400
+ return Promise.resolve();
2401
+ },
2402
+ listen(_handler, _options) {
2403
+ return Promise.resolve();
2404
+ }
2405
+ },
2406
+ outboxRetryPolicy: () => null
2407
+ }).processQueuedTask(void 0, {
2408
+ type: "outbox",
2409
+ id: crypto.randomUUID(),
2410
+ baseUrl: "https://example.com",
2411
+ keys: [],
2412
+ activity: {
2413
+ "@context": "https://www.w3.org/ns/activitystreams",
2414
+ type: "Follow",
2415
+ actor: "https://example.com/users/alice",
2416
+ object: "https://remote.example/users/bob"
2417
+ },
2418
+ activityType: "https://www.w3.org/ns/activitystreams#Follow",
2419
+ inbox: "https://invalid-domain-that-does-not-exist.example/inbox",
2420
+ sharedInbox: false,
2421
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2422
+ attempt: 0,
2423
+ headers: {},
2424
+ traceContext: {}
2425
+ });
2426
+ const outboxLifecycle = recorder.getMeasurements("activitypub.outbox.activity");
2427
+ assertEquals(outboxLifecycle.length, 1);
2428
+ assertEquals(outboxLifecycle[0].type, "counter");
2429
+ assertEquals(outboxLifecycle[0].attributes["activitypub.processing.result"], "abandoned");
2430
+ assertEquals(outboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Follow");
2431
+ });
2432
+ await t.step("records activitypub.inbox.activity processed on successful queued dispatch", async () => {
2433
+ const kv = new MemoryKvStore();
2434
+ const [meterProvider, recorder] = createTestMeterProvider();
2435
+ const federation = new FederationImpl({
2436
+ kv,
2437
+ meterProvider,
2438
+ queue: {
2439
+ enqueue(_message, _options) {
2440
+ return Promise.resolve();
2441
+ },
2442
+ listen(_handler, _options) {
2443
+ return Promise.resolve();
2444
+ }
2445
+ }
2446
+ });
2447
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {});
2448
+ await federation.processQueuedTask(void 0, {
2449
+ type: "inbox",
2450
+ id: crypto.randomUUID(),
2451
+ baseUrl: "https://example.com",
2452
+ activity: {
2453
+ "@context": "https://www.w3.org/ns/activitystreams",
2454
+ type: "Create",
2455
+ id: "https://example.com/activities/queued-processed",
2456
+ actor: "https://remote.example/users/alice",
2457
+ object: {
2458
+ type: "Note",
2459
+ content: "Hello world"
2460
+ }
2461
+ },
2462
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2463
+ attempt: 0,
2464
+ identifier: null,
2465
+ traceContext: {}
2466
+ });
2467
+ const inboxLifecycle = recorder.getMeasurements("activitypub.inbox.activity");
2468
+ assertEquals(inboxLifecycle.length, 1);
2469
+ assertEquals(inboxLifecycle[0].type, "counter");
2470
+ assertEquals(inboxLifecycle[0].attributes["activitypub.processing.result"], "processed");
2471
+ assertEquals(inboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2472
+ });
2473
+ await t.step("records activitypub.inbox.activity retried on transient listener failure", async () => {
2474
+ const kv = new MemoryKvStore();
2475
+ const [meterProvider, recorder] = createTestMeterProvider();
2476
+ const federation = new FederationImpl({
2477
+ kv,
2478
+ meterProvider,
2479
+ queue: {
2480
+ enqueue(_message, _options) {
2481
+ return Promise.resolve();
2482
+ },
2483
+ listen(_handler, _options) {
2484
+ return Promise.resolve();
2485
+ }
2486
+ }
2487
+ });
2488
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {
2489
+ throw new Error("Intended error for testing");
2490
+ });
2491
+ await federation.processQueuedTask(void 0, {
2492
+ type: "inbox",
2493
+ id: crypto.randomUUID(),
2494
+ baseUrl: "https://example.com",
2495
+ activity: {
2496
+ "@context": "https://www.w3.org/ns/activitystreams",
2497
+ type: "Create",
2498
+ id: "https://example.com/activities/queued-retried",
2499
+ actor: "https://remote.example/users/alice",
2500
+ object: {
2501
+ type: "Note",
2502
+ content: "Hello world"
2503
+ }
2504
+ },
2505
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2506
+ attempt: 0,
2507
+ identifier: null,
2508
+ traceContext: {}
2509
+ });
2510
+ const inboxLifecycle = recorder.getMeasurements("activitypub.inbox.activity");
2511
+ assertEquals(inboxLifecycle.length, 1);
2512
+ assertEquals(inboxLifecycle[0].attributes["activitypub.processing.result"], "retried");
2513
+ assertEquals(inboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2514
+ });
2515
+ await t.step("records activitypub.inbox.activity abandoned when retry policy gives up", async () => {
2516
+ const kv = new MemoryKvStore();
2517
+ const [meterProvider, recorder] = createTestMeterProvider();
2518
+ const federation = new FederationImpl({
2519
+ kv,
2520
+ meterProvider,
2521
+ queue: {
2522
+ enqueue(_message, _options) {
2523
+ return Promise.resolve();
2524
+ },
2525
+ listen(_handler, _options) {
2526
+ return Promise.resolve();
2527
+ }
2528
+ },
2529
+ inboxRetryPolicy: () => null
2530
+ });
2531
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {
2532
+ throw new Error("Intended error for testing");
2533
+ });
2534
+ await federation.processQueuedTask(void 0, {
2535
+ type: "inbox",
2536
+ id: crypto.randomUUID(),
2537
+ baseUrl: "https://example.com",
2538
+ activity: {
2539
+ "@context": "https://www.w3.org/ns/activitystreams",
2540
+ type: "Create",
2541
+ id: "https://example.com/activities/queued-abandoned",
2542
+ actor: "https://remote.example/users/alice",
2543
+ object: {
2544
+ type: "Note",
2545
+ content: "Hello world"
2546
+ }
2547
+ },
2548
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2549
+ attempt: 0,
2550
+ identifier: null,
2551
+ traceContext: {}
2552
+ });
2553
+ const inboxLifecycle = recorder.getMeasurements("activitypub.inbox.activity");
2554
+ assertEquals(inboxLifecycle.length, 1);
2555
+ assertEquals(inboxLifecycle[0].attributes["activitypub.processing.result"], "abandoned");
2556
+ assertEquals(inboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2557
+ });
2558
+ await t.step("records queued inbox processing duration", async () => {
2559
+ const kv = new MemoryKvStore();
2560
+ const [meterProvider, recorder] = createTestMeterProvider();
2561
+ const federation = new FederationImpl({
2562
+ kv,
2563
+ meterProvider,
2564
+ queue: {
2565
+ enqueue(_message, _options) {
2566
+ return Promise.resolve();
2567
+ },
2568
+ listen(_handler, _options) {
2569
+ return Promise.resolve();
2570
+ }
2571
+ }
2572
+ });
2573
+ let handled = false;
2574
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {
2575
+ handled = true;
2576
+ });
2577
+ await federation.processQueuedTask(void 0, {
2578
+ type: "inbox",
2579
+ id: crypto.randomUUID(),
2580
+ baseUrl: "https://example.com",
2581
+ activity: {
2582
+ "@context": "https://www.w3.org/ns/activitystreams",
2583
+ type: "Create",
2584
+ id: "https://remote.example/activities/1",
2585
+ actor: "https://remote.example/users/alice",
2586
+ object: {
2587
+ type: "Note",
2588
+ content: "Hello world"
2589
+ }
2590
+ },
2591
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2592
+ attempt: 0,
2593
+ identifier: null,
2594
+ traceContext: {}
2595
+ });
2596
+ assert(handled);
2597
+ const durations = recorder.getMeasurements("activitypub.inbox.processing_duration");
2598
+ assertEquals(durations.length, 1);
2599
+ assertEquals(durations[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2600
+ const started = recorder.getMeasurements("fedify.queue.task.started");
2601
+ assertEquals(started.length, 1);
2602
+ assertEquals(started[0].attributes["fedify.queue.role"], "inbox");
2603
+ const completed = recorder.getMeasurements("fedify.queue.task.completed");
2604
+ assertEquals(completed.length, 1);
2605
+ assertEquals(completed[0].attributes["fedify.queue.role"], "inbox");
2606
+ assertEquals(completed[0].attributes["fedify.queue.task.result"], "completed");
2607
+ assertEquals(completed[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2608
+ assertEquals(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
2609
+ const taskDurations = recorder.getMeasurements("fedify.queue.task.duration");
2610
+ assertEquals(taskDurations.length, 1);
2611
+ assertEquals(taskDurations[0].type, "histogram");
2612
+ assertEquals(taskDurations[0].attributes["fedify.queue.role"], "inbox");
2613
+ assertEquals(taskDurations[0].attributes["fedify.queue.task.result"], "completed");
2614
+ const inFlight = recorder.getMeasurements("fedify.queue.task.in_flight");
2615
+ assertEquals(inFlight.length, 2);
2616
+ assertEquals(inFlight[0].type, "upDownCounter");
2617
+ assertEquals(inFlight[0].value, 1);
2618
+ assertEquals(inFlight[1].value, -1);
2619
+ assertEquals(inFlight[0].attributes, inFlight[1].attributes);
2620
+ assertEquals(inFlight[0].attributes["fedify.queue.role"], "inbox");
2621
+ assertEquals(inFlight[0].attributes["activitypub.activity.type"], void 0);
2622
+ });
2623
+ await t.step("with restrictive context loader and normalized LD-signed inbox activity", async () => {
2624
+ const remoteContextUrl = "https://remote.example/contexts/ext";
2625
+ const sourceContextLoader = async (resource) => {
2626
+ const url = new URL(resource).href;
2627
+ if (url === remoteContextUrl) return {
2628
+ contextUrl: null,
2629
+ documentUrl: url,
2630
+ document: { "@context": { ext: "https://example.com/ext" } }
2631
+ };
2632
+ return await mockDocumentLoader(url);
2633
+ };
2634
+ const restrictiveContextLoader = async (resource) => {
2635
+ const url = new URL(resource).href;
2636
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
2637
+ throw new Error(`Unexpected context: ${url}`);
2638
+ };
2639
+ const kv = new MemoryKvStore();
2640
+ let receivedCount = 0;
2641
+ let received = null;
2642
+ let receivedRaw = null;
2643
+ const federation = new FederationImpl({
2644
+ kv,
2645
+ contextLoaderFactory: () => restrictiveContextLoader
2646
+ });
2647
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, (ctx, activity) => {
2648
+ receivedCount++;
2649
+ receivedRaw = ctx.activity;
2650
+ received = activity;
2651
+ });
2652
+ const signed = await signJsonLd({
2653
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
2654
+ id: "https://remote.example/activities/1",
2655
+ type: "Create",
2656
+ actor: "https://remote.example/users/alice",
2657
+ ext: "preserve-me",
2658
+ object: {
2659
+ id: "https://remote.example/notes/1",
2660
+ type: "Note",
2661
+ attributedTo: "https://remote.example/users/alice",
2662
+ content: "Hello, world!"
2663
+ }
2664
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: sourceContextLoader });
2665
+ const normalizedActivity = await compactJsonLd(signed, sourceContextLoader);
2666
+ const messageId = crypto.randomUUID();
2667
+ await federation.processQueuedTask(void 0, {
2668
+ type: "inbox",
2669
+ id: messageId,
2670
+ baseUrl: "https://example.com",
2671
+ activity: signed,
2672
+ normalizedActivity,
2673
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2674
+ attempt: 0,
2675
+ identifier: null,
2676
+ traceContext: {}
2677
+ });
2678
+ const delivered = received;
2679
+ assert(delivered != null);
2680
+ const deliveredCreate = delivered;
2681
+ assertInstanceOf(deliveredCreate, Create);
2682
+ assertEquals(deliveredCreate.id?.href, "https://remote.example/activities/1");
2683
+ assertEquals(receivedRaw, signed);
2684
+ await federation.processQueuedTask(void 0, {
2685
+ type: "inbox",
2686
+ id: messageId,
2687
+ baseUrl: "https://example.com",
2688
+ activity: signed,
2689
+ normalizedActivity,
2690
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2691
+ attempt: 0,
2692
+ identifier: null,
2693
+ traceContext: {}
2694
+ });
2695
+ assertEquals(receivedCount, 1);
2696
+ });
2697
+ await t.step("cached normalizedActivity is rechecked for unsafe JSON-LD keywords", async () => {
2698
+ const queuedMessages = [];
2699
+ const queue = {
2700
+ enqueue(message, _options) {
2701
+ queuedMessages.push(message);
2702
+ return Promise.resolve();
2703
+ },
2704
+ listen(_handler, _options) {
2705
+ return Promise.resolve();
2706
+ }
2707
+ };
2708
+ const kv = new MemoryKvStore();
2709
+ let receivedCount = 0;
2710
+ let errorCount = 0;
2711
+ const federation = new FederationImpl({
2712
+ kv,
2713
+ queue
2714
+ });
2715
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
2716
+ receivedCount++;
2717
+ }).onError(() => {
2718
+ errorCount++;
2719
+ });
2720
+ const signed = await signJsonLd({
2721
+ "@context": "https://www.w3.org/ns/activitystreams",
2722
+ id: "https://remote.example/activities/unsafe-normalized-cache",
2723
+ type: "Create",
2724
+ actor: "https://remote.example/users/alice",
2725
+ object: {
2726
+ id: "https://remote.example/notes/unsafe-normalized-cache",
2727
+ type: "Note",
2728
+ attributedTo: "https://remote.example/users/alice",
2729
+ content: "Hello from unsafe normalized cache"
2730
+ }
2731
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
2732
+ const normalizedActivity = await compactJsonLd(signed, mockDocumentLoader);
2733
+ const tamperedNormalizedActivity = {
2734
+ ...normalizedActivity,
2735
+ signature: {
2736
+ ...normalizedActivity.signature,
2737
+ "@included": [{
2738
+ id: "https://remote.example/activities/inside-signature",
2739
+ type: "Undo"
2740
+ }]
2741
+ }
2742
+ };
2743
+ await federation.processQueuedTask(void 0, {
2744
+ type: "inbox",
2745
+ id: crypto.randomUUID(),
2746
+ baseUrl: "https://example.com",
2747
+ activity: signed,
2748
+ normalizedActivity: tamperedNormalizedActivity,
2749
+ ldSignatureVerified: false,
2750
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2751
+ attempt: 0,
2752
+ identifier: null,
2753
+ traceContext: {}
2754
+ });
2755
+ assertEquals(receivedCount, 0);
2756
+ assertEquals(errorCount, 1);
2757
+ assertEquals(queuedMessages, []);
2758
+ });
2759
+ await t.step("old queued LDS inbox messages without normalizedActivity still work", async () => {
2760
+ const restrictiveContextLoader = async (resource) => {
2761
+ const url = new URL(resource).href;
2762
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
2763
+ throw new Error(`Unexpected context: ${url}`);
2764
+ };
2765
+ const kv = new MemoryKvStore();
2766
+ let received = null;
2767
+ const federation = new FederationImpl({
2768
+ kv,
2769
+ contextLoaderFactory: () => restrictiveContextLoader
2770
+ });
2771
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, (_ctx, activity) => {
2772
+ received = activity;
2773
+ });
2774
+ const compacted = await compactJsonLd(await signJsonLd({
2775
+ "@context": "https://www.w3.org/ns/activitystreams",
2776
+ id: "https://remote.example/activities/legacy",
2777
+ type: "Create",
2778
+ actor: "https://remote.example/users/alice",
2779
+ object: {
2780
+ id: "https://remote.example/notes/legacy",
2781
+ type: "Note",
2782
+ attributedTo: "https://remote.example/users/alice",
2783
+ content: "Hello from legacy queue"
2784
+ }
2785
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }), restrictiveContextLoader);
2786
+ await federation.processQueuedTask(void 0, {
2787
+ type: "inbox",
2788
+ id: crypto.randomUUID(),
2789
+ baseUrl: "https://example.com",
2790
+ activity: compacted,
2791
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2792
+ attempt: 0,
2793
+ identifier: null,
2794
+ traceContext: {}
2795
+ });
2796
+ assert(received != null);
2797
+ assertEquals(received.id?.href, "https://remote.example/activities/legacy");
2798
+ });
2799
+ await t.step("queued signature-bearing non-LDS inbox messages keep parse-time normalization contexts", async () => {
2800
+ const signingContextLoader = async (resource) => {
2801
+ const url = new URL(resource).href;
2802
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1" || url === "https://w3id.org/security/v1" || url === "https://w3id.org/security/data-integrity/v1") return await mockDocumentLoader(url);
2803
+ throw new Error(`Unexpected context: ${url}`);
2804
+ };
2805
+ const processingContextLoader = async (resource) => {
2806
+ const url = new URL(resource).href;
2807
+ if (url === "https://w3id.org/identity/v1" || url === "https://w3id.org/security/v1" || url === "https://w3id.org/security/data-integrity/v1") throw new Error("queued non-LDS signed payloads should parse with the normalization loader's built-in signature contexts");
2808
+ return await signingContextLoader(resource);
2809
+ };
2810
+ const kv = new MemoryKvStore();
2811
+ let received = null;
2812
+ let receivedRaw = null;
2813
+ const federation = new FederationImpl({
2814
+ kv,
2815
+ contextLoaderFactory: () => processingContextLoader
2816
+ });
2817
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, (ctx, activity) => {
2818
+ receivedRaw = ctx.activity;
2819
+ received = activity;
2820
+ });
2821
+ const signed = await signJsonLd({
2822
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
2823
+ id: "https://remote.example/activities/non-lds-queued-signature",
2824
+ type: "Create",
2825
+ actor: "https://remote.example/users/alice",
2826
+ object: {
2827
+ id: "https://remote.example/notes/non-lds-queued-signature",
2828
+ type: "Note",
2829
+ attributedTo: "https://remote.example/users/alice",
2830
+ content: "Hello from non-LDS queued signature"
2831
+ }
2832
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: signingContextLoader });
2833
+ const signedPayload = signed;
2834
+ assert(Array.isArray(signedPayload["@context"]) && signedPayload["@context"].includes("https://w3id.org/security/v1"));
2835
+ await federation.processQueuedTask(void 0, {
2836
+ type: "inbox",
2837
+ id: crypto.randomUUID(),
2838
+ baseUrl: "https://example.com",
2839
+ activity: signed,
2840
+ ldSignatureVerified: false,
2841
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2842
+ attempt: 0,
2843
+ identifier: null,
2844
+ traceContext: {}
2845
+ });
2846
+ if (received == null) throw new Error("Inbox activity not delivered.");
2847
+ assertEquals(received.id?.href, "https://remote.example/activities/non-lds-queued-signature");
2848
+ assertEquals(receivedRaw, signed);
2849
+ });
2850
+ await t.step("queued signature-bearing non-LDS inbox messages reuse normalizedActivity for custom contexts", async () => {
2851
+ const remoteContextUrl = "https://remote.example/contexts/ext";
2852
+ const sourceContextLoader = async (resource) => {
2853
+ const url = new URL(resource).href;
2854
+ if (url === remoteContextUrl) return {
2855
+ contextUrl: null,
2856
+ documentUrl: url,
2857
+ document: { "@context": { ext: "https://example.com/ext" } }
2858
+ };
2859
+ return await mockDocumentLoader(url);
2860
+ };
2861
+ const restrictiveContextLoader = async (resource) => {
2862
+ const url = new URL(resource).href;
2863
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
2864
+ throw new Error(`Unexpected context: ${url}`);
2865
+ };
2866
+ const kv = new MemoryKvStore();
2867
+ let received = null;
2868
+ let receivedRaw = null;
2869
+ const federation = new FederationImpl({
2870
+ kv,
2871
+ contextLoaderFactory: () => restrictiveContextLoader
2872
+ });
2873
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, (ctx, activity) => {
2874
+ receivedRaw = ctx.activity;
2875
+ received = activity;
2876
+ });
2877
+ const unsignedBody = {
2878
+ "@context": [
2879
+ remoteContextUrl,
2880
+ "https://www.w3.org/ns/activitystreams",
2881
+ "https://w3id.org/security/v1"
2882
+ ],
2883
+ id: "https://remote.example/activities/non-lds-queued-custom-context",
2884
+ type: "Create",
2885
+ actor: "https://remote.example/users/alice",
2886
+ ext: "preserve-me",
2887
+ object: {
2888
+ id: "https://remote.example/notes/non-lds-queued-custom-context",
2889
+ type: "Note",
2890
+ attributedTo: "https://remote.example/users/alice",
2891
+ content: "Hello from non-LDS queued custom context"
2892
+ },
2893
+ signature: {
2894
+ type: "RsaSignature2017",
2895
+ creator: "not a url",
2896
+ created: "2024-09-12T16:50:46Z",
2897
+ signatureValue: "Zm9v"
2898
+ }
2899
+ };
2900
+ const normalizedActivity = await compactJsonLd(unsignedBody, sourceContextLoader);
2901
+ await federation.processQueuedTask(void 0, {
2902
+ type: "inbox",
2903
+ id: crypto.randomUUID(),
2904
+ baseUrl: "https://example.com",
2905
+ activity: unsignedBody,
2906
+ normalizedActivity,
2907
+ ldSignatureVerified: false,
2908
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2909
+ attempt: 0,
2910
+ identifier: null,
2911
+ traceContext: {}
2912
+ });
2913
+ if (received == null) throw new Error("Inbox activity not delivered.");
2914
+ assertEquals(received.id?.href, "https://remote.example/activities/non-lds-queued-custom-context");
2915
+ assertEquals(receivedRaw, unsignedBody);
2916
+ });
2917
+ await t.step("legacy raw LDS inbox messages without normalizedActivity retry through worker error handling", async () => {
2918
+ const remoteContextUrl = "https://remote.example/contexts/ext";
2919
+ const restrictiveContextLoader = async (resource) => {
2920
+ const url = new URL(resource).href;
2921
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
2922
+ throw new Error(`Unexpected context: ${url}`);
2923
+ };
2924
+ const queue = {
2925
+ enqueue(message, _options) {
2926
+ queuedMessages.push(message);
2927
+ return Promise.resolve();
2928
+ },
2929
+ listen(_handler, _options) {
2930
+ return Promise.resolve();
2931
+ }
2932
+ };
2933
+ const kv = new MemoryKvStore();
2934
+ const queuedMessages = [];
2935
+ let errorCount = 0;
2936
+ const federation = new FederationImpl({
2937
+ kv,
2938
+ queue,
2939
+ contextLoaderFactory: () => restrictiveContextLoader
2940
+ });
2941
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
2942
+ throw new Error("listener should not run");
2943
+ }).onError(() => {
2944
+ errorCount++;
2945
+ });
2946
+ const signed = await signJsonLd({
2947
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
2948
+ id: "https://remote.example/activities/legacy-raw",
2949
+ type: "Create",
2950
+ actor: "https://remote.example/users/alice",
2951
+ ext: "preserve-me",
2952
+ object: {
2953
+ id: "https://remote.example/notes/legacy-raw",
2954
+ type: "Note",
2955
+ attributedTo: "https://remote.example/users/alice",
2956
+ content: "Hello from raw legacy queue"
2957
+ }
2958
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
2959
+ const url = new URL(resource).href;
2960
+ if (url === remoteContextUrl) return {
2961
+ contextUrl: null,
2962
+ documentUrl: url,
2963
+ document: { "@context": { ext: "https://example.com/ext" } }
2964
+ };
2965
+ return await mockDocumentLoader(url);
2966
+ } });
2967
+ const inboxMessage = {
2968
+ type: "inbox",
2969
+ id: crypto.randomUUID(),
2970
+ baseUrl: "https://example.com",
2971
+ activity: signed,
2972
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2973
+ attempt: 0,
2974
+ identifier: null,
2975
+ traceContext: {}
2976
+ };
2977
+ await federation.processQueuedTask(void 0, inboxMessage);
2978
+ assertEquals(errorCount, 1);
2979
+ assertEquals(queuedMessages.length, 1);
2980
+ const retried = queuedMessages[0];
2981
+ assertEquals(retried.attempt, 1);
2982
+ assertEquals(retried.activity, inboxMessage.activity);
2983
+ });
2984
+ await t.step("without inbox queue retriable inbox parse failures bubble to caller", async () => {
2985
+ const remoteContextUrl = "https://remote.example/contexts/ext";
2986
+ const sourceContextLoader = async (resource) => {
2987
+ const url = new URL(resource).href;
2988
+ if (url === remoteContextUrl) return {
2989
+ contextUrl: null,
2990
+ documentUrl: url,
2991
+ document: { "@context": { ext: "https://example.com/ext" } }
2992
+ };
2993
+ return await mockDocumentLoader(url);
2994
+ };
2995
+ let errorCount = 0;
2996
+ const federation = new FederationImpl({
2997
+ kv: new MemoryKvStore(),
2998
+ contextLoaderFactory: () => async (resource) => {
2999
+ const url = new URL(resource).href;
3000
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3001
+ if (url === remoteContextUrl) throw new Error(`Transient remote context failure: ${url}`);
3002
+ throw new Error(`Unexpected context: ${url}`);
3003
+ }
3004
+ });
3005
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3006
+ throw new Error("listener should not run");
3007
+ }).onError(() => {
3008
+ errorCount++;
3009
+ });
3010
+ const signed = await signJsonLd({
3011
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3012
+ id: "https://remote.example/activities/manual-retry",
3013
+ type: "Create",
3014
+ actor: "https://remote.example/users/alice",
3015
+ ext: "preserve-me",
3016
+ object: {
3017
+ id: "https://remote.example/notes/manual-retry",
3018
+ type: "Note",
3019
+ attributedTo: "https://remote.example/users/alice",
3020
+ content: "Hello from manual retry queue"
3021
+ }
3022
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: sourceContextLoader });
3023
+ await assertRejects(() => federation.processQueuedTask(void 0, {
3024
+ type: "inbox",
3025
+ id: crypto.randomUUID(),
3026
+ baseUrl: "https://example.com",
3027
+ activity: signed,
3028
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3029
+ attempt: 0,
3030
+ identifier: null,
3031
+ traceContext: {}
3032
+ }), Error);
3033
+ assertEquals(errorCount, 1);
3034
+ });
3035
+ await t.step("legacy raw LDS inbox messages with transient InvalidUrl failures retry", async () => {
3036
+ const remoteContextUrl = "https://remote.example/contexts/ext";
3037
+ const queue = {
3038
+ enqueue(message, _options) {
3039
+ queuedMessages.push(message);
3040
+ return Promise.resolve();
3041
+ },
3042
+ listen(_handler, _options) {
3043
+ return Promise.resolve();
3044
+ }
3045
+ };
3046
+ const kv = new MemoryKvStore();
3047
+ const queuedMessages = [];
3048
+ let errorCount = 0;
3049
+ const federation = new FederationImpl({
3050
+ kv,
3051
+ queue,
3052
+ contextLoaderFactory: () => async (resource) => {
3053
+ const url = new URL(resource).href;
3054
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3055
+ if (url === remoteContextUrl) {
3056
+ const error = /* @__PURE__ */ new Error(`Transient remote context failure: ${url}`);
3057
+ error.name = "jsonld.InvalidUrl";
3058
+ error.details = {
3059
+ code: "loading remote context failed",
3060
+ url
3061
+ };
3062
+ throw error;
3063
+ }
3064
+ throw new Error(`Unexpected context: ${url}`);
3065
+ }
3066
+ });
3067
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3068
+ throw new Error("listener should not run");
3069
+ }).onError(() => {
3070
+ errorCount++;
3071
+ });
3072
+ const signed = await signJsonLd({
3073
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3074
+ id: "https://remote.example/activities/legacy-invalid-context",
3075
+ type: "Create",
3076
+ actor: "https://remote.example/users/alice",
3077
+ ext: "preserve-me",
3078
+ object: {
3079
+ id: "https://remote.example/notes/legacy-invalid-context",
3080
+ type: "Note",
3081
+ attributedTo: "https://remote.example/users/alice",
3082
+ content: "Hello from invalid legacy queue"
3083
+ }
3084
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
3085
+ const url = new URL(resource).href;
3086
+ if (url === remoteContextUrl) return {
3087
+ contextUrl: null,
3088
+ documentUrl: url,
3089
+ document: { "@context": { ext: "https://example.com/ext" } }
3090
+ };
3091
+ return await mockDocumentLoader(url);
3092
+ } });
3093
+ const inboxMessage = {
3094
+ type: "inbox",
3095
+ id: crypto.randomUUID(),
3096
+ baseUrl: "https://example.com",
3097
+ activity: signed,
3098
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3099
+ attempt: 0,
3100
+ identifier: null,
3101
+ traceContext: {}
3102
+ };
3103
+ await federation.processQueuedTask(void 0, inboxMessage);
3104
+ assertEquals(errorCount, 1);
3105
+ assertEquals(queuedMessages.length, 1);
3106
+ const retried = queuedMessages[0];
3107
+ assertEquals(retried.attempt, 1);
3108
+ assertEquals(retried.activity, inboxMessage.activity);
3109
+ });
3110
+ await t.step("legacy raw LDS inbox messages with opaque context ids retry", async () => {
3111
+ const queue = {
3112
+ enqueue(message, _options) {
3113
+ queuedMessages.push(message);
3114
+ return Promise.resolve();
3115
+ },
3116
+ listen(_handler, _options) {
3117
+ return Promise.resolve();
3118
+ }
3119
+ };
3120
+ const kv = new MemoryKvStore();
3121
+ const queuedMessages = [];
3122
+ let errorCount = 0;
3123
+ const federation = new FederationImpl({
3124
+ kv,
3125
+ queue,
3126
+ contextLoaderFactory: () => async (resource) => {
3127
+ if (resource === "app-context") {
3128
+ const error = /* @__PURE__ */ new Error(`Opaque context backend is unavailable: ${resource}`);
3129
+ error.name = "jsonld.InvalidUrl";
3130
+ error.details = {
3131
+ code: "loading remote context failed",
3132
+ url: resource
3133
+ };
3134
+ throw error;
3135
+ }
3136
+ const url = new URL(resource).href;
3137
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3138
+ throw new Error(`Unexpected context: ${resource}`);
3139
+ }
3140
+ });
3141
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3142
+ throw new Error("listener should not run");
3143
+ }).onError(() => {
3144
+ errorCount++;
3145
+ });
3146
+ const signed = await signJsonLd({
3147
+ "@context": "https://www.w3.org/ns/activitystreams",
3148
+ id: "https://remote.example/activities/legacy-malformed-context",
3149
+ type: "Create",
3150
+ actor: "https://remote.example/users/alice",
3151
+ object: {
3152
+ id: "https://remote.example/notes/legacy-malformed-context",
3153
+ type: "Note",
3154
+ attributedTo: "https://remote.example/users/alice",
3155
+ content: "Hello from malformed legacy queue"
3156
+ }
3157
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3158
+ const inboxMessage = {
3159
+ type: "inbox",
3160
+ id: crypto.randomUUID(),
3161
+ baseUrl: "https://example.com",
3162
+ activity: {
3163
+ ...signed,
3164
+ "@context": ["app-context", "https://www.w3.org/ns/activitystreams"]
3165
+ },
3166
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3167
+ attempt: 0,
3168
+ identifier: null,
3169
+ traceContext: {}
3170
+ };
3171
+ await federation.processQueuedTask(void 0, inboxMessage);
3172
+ assertEquals(errorCount, 1);
3173
+ assertEquals(queuedMessages, [{
3174
+ ...inboxMessage,
3175
+ attempt: 1
3176
+ }]);
3177
+ });
3178
+ await t.step("legacy raw LDS inbox messages with Invalid URL TypeErrors retry", async () => {
3179
+ const queue = {
3180
+ enqueue(message, _options) {
3181
+ queuedMessages.push(message);
3182
+ return Promise.resolve();
3183
+ },
3184
+ listen(_handler, _options) {
3185
+ return Promise.resolve();
3186
+ }
3187
+ };
3188
+ const kv = new MemoryKvStore();
3189
+ const queuedMessages = [];
3190
+ let errorCount = 0;
3191
+ const federation = new FederationImpl({
3192
+ kv,
3193
+ queue,
3194
+ contextLoaderFactory: () => async (resource) => {
3195
+ if (resource === "app:context") throw new TypeError(`Invalid URL: ${resource}`);
3196
+ const url = new URL(resource).href;
3197
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3198
+ throw new Error(`Unexpected context: ${resource}`);
3199
+ }
3200
+ });
3201
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3202
+ throw new Error("listener should not run");
3203
+ }).onError(() => {
3204
+ errorCount++;
3205
+ });
3206
+ const signed = await signJsonLd({
3207
+ "@context": "https://www.w3.org/ns/activitystreams",
3208
+ id: "https://remote.example/activities/legacy-typeerror-invalid-url",
3209
+ type: "Create",
3210
+ actor: "https://remote.example/users/alice",
3211
+ object: {
3212
+ id: "https://remote.example/notes/legacy-typeerror-invalid-url",
3213
+ type: "Note",
3214
+ attributedTo: "https://remote.example/users/alice",
3215
+ content: "Hello from invalid-url typeerror queue"
3216
+ }
3217
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3218
+ const inboxMessage = {
3219
+ type: "inbox",
3220
+ id: crypto.randomUUID(),
3221
+ baseUrl: "https://example.com",
3222
+ activity: {
3223
+ ...signed,
3224
+ "@context": ["app:context", "https://www.w3.org/ns/activitystreams"]
3225
+ },
3226
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3227
+ attempt: 0,
3228
+ identifier: null,
3229
+ traceContext: {}
3230
+ };
3231
+ await federation.processQueuedTask(void 0, inboxMessage);
3232
+ assertEquals(errorCount, 1);
3233
+ assertEquals(queuedMessages.length, 1);
3234
+ const retried = queuedMessages[0];
3235
+ assertEquals(retried.attempt, 1);
3236
+ assertEquals(retried.activity, inboxMessage.activity);
3237
+ });
3238
+ await t.step("legacy raw LDS inbox messages with malformed absolute context refs do not retry", async () => {
3239
+ const queue = {
3240
+ enqueue(message, _options) {
3241
+ queuedMessages.push(message);
3242
+ return Promise.resolve();
3243
+ },
3244
+ listen(_handler, _options) {
3245
+ return Promise.resolve();
3246
+ }
3247
+ };
3248
+ const kv = new MemoryKvStore();
3249
+ const queuedMessages = [];
3250
+ let errorCount = 0;
3251
+ const federation = new FederationImpl({
3252
+ kv,
3253
+ queue,
3254
+ contextLoaderFactory: () => async (resource) => {
3255
+ if (resource === "http:/[") throw new TypeError(`Invalid URL: ${resource}`);
3256
+ const url = new URL(resource).href;
3257
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3258
+ throw new Error(`Unexpected context: ${resource}`);
3259
+ }
3260
+ });
3261
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3262
+ throw new Error("listener should not run");
3263
+ }).onError(() => {
3264
+ errorCount++;
3265
+ });
3266
+ const signed = await signJsonLd({
3267
+ "@context": "https://www.w3.org/ns/activitystreams",
3268
+ id: "https://remote.example/activities/legacy-malformed-absolute-context",
3269
+ type: "Create",
3270
+ actor: "https://remote.example/users/alice",
3271
+ object: {
3272
+ id: "https://remote.example/notes/legacy-malformed-absolute-context",
3273
+ type: "Note",
3274
+ attributedTo: "https://remote.example/users/alice",
3275
+ content: "Hello from malformed absolute context queue"
3276
+ }
3277
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3278
+ const inboxMessage = {
3279
+ type: "inbox",
3280
+ id: crypto.randomUUID(),
3281
+ baseUrl: "https://example.com",
3282
+ activity: {
3283
+ ...signed,
3284
+ "@context": ["http:/[", "https://www.w3.org/ns/activitystreams"]
3285
+ },
3286
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3287
+ attempt: 0,
3288
+ identifier: null,
3289
+ traceContext: {}
3290
+ };
3291
+ await federation.processQueuedTask(void 0, inboxMessage);
3292
+ assertEquals(errorCount, 1);
3293
+ assertEquals(queuedMessages, []);
3294
+ });
3295
+ await t.step("malformed IRI fields are permanent queued inbox parse errors", async () => {
3296
+ const queuedMessages = [];
3297
+ const queue = {
3298
+ enqueue(message, _options) {
3299
+ queuedMessages.push(message);
3300
+ return Promise.resolve();
3301
+ },
3302
+ listen(_handler, _options) {
3303
+ return Promise.resolve();
3304
+ }
3305
+ };
3306
+ const kv = new MemoryKvStore();
3307
+ let errorCount = 0;
3308
+ const federation = new FederationImpl({
3309
+ kv,
3310
+ queue
3311
+ });
3312
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3313
+ throw new Error("listener should not run");
3314
+ }).onError(() => {
3315
+ errorCount++;
3316
+ });
3317
+ await federation.processQueuedTask(void 0, {
3318
+ type: "inbox",
3319
+ id: crypto.randomUUID(),
3320
+ baseUrl: "https://example.com",
3321
+ activity: {
3322
+ "@context": "https://www.w3.org/ns/activitystreams",
3323
+ id: "http://[",
3324
+ type: "Create",
3325
+ actor: "https://remote.example/users/alice",
3326
+ object: {
3327
+ id: "https://remote.example/notes/invalid-iri",
3328
+ type: "Note",
3329
+ attributedTo: "https://remote.example/users/alice",
3330
+ content: "Hello from invalid IRI queue"
3331
+ }
3332
+ },
3333
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3334
+ attempt: 0,
3335
+ identifier: null,
3336
+ traceContext: {}
3337
+ });
3338
+ assertEquals(errorCount, 1);
3339
+ assertEquals(queuedMessages, []);
3340
+ });
3341
+ await t.step("legacy raw LDS inbox messages with network-path context ids retry", async () => {
3342
+ const queue = {
3343
+ enqueue(message, _options) {
3344
+ queuedMessages.push(message);
3345
+ return Promise.resolve();
3346
+ },
3347
+ listen(_handler, _options) {
3348
+ return Promise.resolve();
3349
+ }
3350
+ };
3351
+ const kv = new MemoryKvStore();
3352
+ const queuedMessages = [];
3353
+ let errorCount = 0;
3354
+ const federation = new FederationImpl({
3355
+ kv,
3356
+ queue,
3357
+ contextLoaderFactory: () => async (resource) => {
3358
+ if (resource === "//cdn.example/ctx") {
3359
+ const error = /* @__PURE__ */ new Error(`Network-path context backend is unavailable: ${resource}`);
3360
+ error.name = "jsonld.InvalidUrl";
3361
+ error.details = {
3362
+ code: "loading remote context failed",
3363
+ url: resource
3364
+ };
3365
+ throw error;
3366
+ }
3367
+ const url = new URL(resource).href;
3368
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3369
+ throw new Error(`Unexpected context: ${resource}`);
3370
+ }
3371
+ });
3372
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3373
+ throw new Error("listener should not run");
3374
+ }).onError(() => {
3375
+ errorCount++;
3376
+ });
3377
+ const signed = await signJsonLd({
3378
+ "@context": "https://www.w3.org/ns/activitystreams",
3379
+ id: "https://remote.example/activities/legacy-network-path-context",
3380
+ type: "Create",
3381
+ actor: "https://remote.example/users/alice",
3382
+ object: {
3383
+ id: "https://remote.example/notes/legacy-network-path-context",
3384
+ type: "Note",
3385
+ attributedTo: "https://remote.example/users/alice",
3386
+ content: "Hello from network-path legacy queue"
3387
+ }
3388
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3389
+ const inboxMessage = {
3390
+ type: "inbox",
3391
+ id: crypto.randomUUID(),
3392
+ baseUrl: "https://example.com",
3393
+ activity: {
3394
+ ...signed,
3395
+ "@context": ["//cdn.example/ctx", "https://www.w3.org/ns/activitystreams"]
3396
+ },
3397
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3398
+ attempt: 0,
3399
+ identifier: null,
3400
+ traceContext: {}
3401
+ };
3402
+ await federation.processQueuedTask(void 0, inboxMessage);
3403
+ assertEquals(errorCount, 1);
3404
+ assertEquals(queuedMessages, [{
3405
+ ...inboxMessage,
3406
+ attempt: 1
3407
+ }]);
3408
+ });
3409
+ await t.step("legacy raw LDS inbox messages with malformed network-path refs do not retry", async () => {
3410
+ const queue = {
3411
+ enqueue(message, _options) {
3412
+ queuedMessages.push(message);
3413
+ return Promise.resolve();
3414
+ },
3415
+ listen(_handler, _options) {
3416
+ return Promise.resolve();
3417
+ }
3418
+ };
3419
+ const kv = new MemoryKvStore();
3420
+ const queuedMessages = [];
3421
+ let errorCount = 0;
3422
+ const federation = new FederationImpl({
3423
+ kv,
3424
+ queue,
3425
+ contextLoaderFactory: () => async (resource) => {
3426
+ if (resource === "//[") {
3427
+ const error = /* @__PURE__ */ new Error(`Malformed network-path context: ${resource}`);
3428
+ error.name = "jsonld.InvalidUrl";
3429
+ error.details = {
3430
+ code: "loading remote context failed",
3431
+ url: resource
3432
+ };
3433
+ throw error;
3434
+ }
3435
+ const url = new URL(resource).href;
3436
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3437
+ throw new Error(`Unexpected context: ${resource}`);
3438
+ }
3439
+ });
3440
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3441
+ throw new Error("listener should not run");
3442
+ }).onError(() => {
3443
+ errorCount++;
3444
+ });
3445
+ const signed = await signJsonLd({
3446
+ "@context": "https://www.w3.org/ns/activitystreams",
3447
+ id: "https://remote.example/activities/legacy-malformed-network-path-context",
3448
+ type: "Create",
3449
+ actor: "https://remote.example/users/alice",
3450
+ object: {
3451
+ id: "https://remote.example/notes/legacy-malformed-network-path-context",
3452
+ type: "Note",
3453
+ attributedTo: "https://remote.example/users/alice",
3454
+ content: "Hello from malformed network-path legacy queue"
3455
+ }
3456
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3457
+ const inboxMessage = {
3458
+ type: "inbox",
3459
+ id: crypto.randomUUID(),
3460
+ baseUrl: "https://example.com",
3461
+ activity: {
3462
+ ...signed,
3463
+ "@context": ["//[", "https://www.w3.org/ns/activitystreams"]
3464
+ },
3465
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3466
+ attempt: 0,
3467
+ identifier: null,
3468
+ traceContext: {}
3469
+ };
3470
+ await federation.processQueuedTask(void 0, inboxMessage);
3471
+ assertEquals(errorCount, 1);
3472
+ assertEquals(queuedMessages, []);
3473
+ });
3474
+ await t.step("legacy raw LDS inbox messages with malformed context URLs do not retry", async () => {
3475
+ const queue = {
3476
+ enqueue(message, _options) {
3477
+ queuedMessages.push(message);
3478
+ return Promise.resolve();
3479
+ },
3480
+ listen(_handler, _options) {
3481
+ return Promise.resolve();
3482
+ }
3483
+ };
3484
+ const kv = new MemoryKvStore();
3485
+ const queuedMessages = [];
3486
+ let errorCount = 0;
3487
+ const federation = new FederationImpl({
3488
+ kv,
3489
+ queue,
3490
+ contextLoaderFactory: () => async (resource) => {
3491
+ if (resource === "not a url") {
3492
+ const error = /* @__PURE__ */ new Error(`Invalid remote context URL: ${resource}`);
3493
+ error.name = "jsonld.InvalidUrl";
3494
+ error.details = {
3495
+ code: "loading remote context failed",
3496
+ url: resource
3497
+ };
3498
+ throw error;
3499
+ }
3500
+ const url = new URL(resource).href;
3501
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3502
+ throw new Error(`Unexpected context: ${resource}`);
3503
+ }
3504
+ });
3505
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3506
+ throw new Error("listener should not run");
3507
+ }).onError(() => {
3508
+ errorCount++;
3509
+ });
3510
+ const signed = await signJsonLd({
3511
+ "@context": "https://www.w3.org/ns/activitystreams",
3512
+ id: "https://remote.example/activities/legacy-malformed-context",
3513
+ type: "Create",
3514
+ actor: "https://remote.example/users/alice",
3515
+ object: {
3516
+ id: "https://remote.example/notes/legacy-malformed-context",
3517
+ type: "Note",
3518
+ attributedTo: "https://remote.example/users/alice",
3519
+ content: "Hello from malformed legacy queue"
3520
+ }
3521
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3522
+ const inboxMessage = {
3523
+ type: "inbox",
3524
+ id: crypto.randomUUID(),
3525
+ baseUrl: "https://example.com",
3526
+ activity: {
3527
+ ...signed,
3528
+ "@context": ["not a url", "https://www.w3.org/ns/activitystreams"]
3529
+ },
3530
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3531
+ attempt: 0,
3532
+ identifier: null,
3533
+ traceContext: {}
3534
+ };
3535
+ await federation.processQueuedTask(void 0, inboxMessage);
3536
+ assertEquals(errorCount, 1);
3537
+ assertEquals(queuedMessages, []);
3538
+ });
3539
+ await t.step("legacy raw LDS inbox messages with invalid percent escapes do not retry", async () => {
3540
+ const queue = {
3541
+ enqueue(message, _options) {
3542
+ queuedMessages.push(message);
3543
+ return Promise.resolve();
3544
+ },
3545
+ listen(_handler, _options) {
3546
+ return Promise.resolve();
3547
+ }
3548
+ };
3549
+ const kv = new MemoryKvStore();
3550
+ const queuedMessages = [];
3551
+ let errorCount = 0;
3552
+ const federation = new FederationImpl({
3553
+ kv,
3554
+ queue,
3555
+ contextLoaderFactory: () => async (resource) => {
3556
+ if (resource === "foo%zz") {
3557
+ const error = /* @__PURE__ */ new Error(`Invalid remote context URL: ${resource}`);
3558
+ error.name = "jsonld.InvalidUrl";
3559
+ error.details = {
3560
+ code: "loading remote context failed",
3561
+ url: resource
3562
+ };
3563
+ throw error;
3564
+ }
3565
+ const url = new URL(resource).href;
3566
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3567
+ throw new Error(`Unexpected context: ${resource}`);
3568
+ }
3569
+ });
3570
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3571
+ throw new Error("listener should not run");
3572
+ }).onError(() => {
3573
+ errorCount++;
3574
+ });
3575
+ const signed = await signJsonLd({
3576
+ "@context": "https://www.w3.org/ns/activitystreams",
3577
+ id: "https://remote.example/activities/legacy-malformed-percent-context",
3578
+ type: "Create",
3579
+ actor: "https://remote.example/users/alice",
3580
+ object: {
3581
+ id: "https://remote.example/notes/legacy-malformed-percent-context",
3582
+ type: "Note",
3583
+ attributedTo: "https://remote.example/users/alice",
3584
+ content: "Hello from malformed percent legacy queue"
3585
+ }
3586
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
3587
+ const inboxMessage = {
3588
+ type: "inbox",
3589
+ id: crypto.randomUUID(),
3590
+ baseUrl: "https://example.com",
3591
+ activity: {
3592
+ ...signed,
3593
+ "@context": ["foo%zz", "https://www.w3.org/ns/activitystreams"]
3594
+ },
3595
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3596
+ attempt: 0,
3597
+ identifier: null,
3598
+ traceContext: {}
3599
+ };
3600
+ await federation.processQueuedTask(void 0, inboxMessage);
3601
+ assertEquals(errorCount, 1);
3602
+ assertEquals(queuedMessages, []);
3603
+ });
3604
+ await t.step("legacy raw LDS inbox messages with invalid remote contexts do not retry", async () => {
3605
+ const remoteContextUrl = "https://remote.example/contexts/ext";
3606
+ const queue = {
3607
+ enqueue(message, _options) {
3608
+ queuedMessages.push(message);
3609
+ return Promise.resolve();
3610
+ },
3611
+ listen(_handler, _options) {
3612
+ return Promise.resolve();
3613
+ }
3614
+ };
3615
+ const kv = new MemoryKvStore();
3616
+ const queuedMessages = [];
3617
+ let errorCount = 0;
3618
+ const federation = new FederationImpl({
3619
+ kv,
3620
+ queue,
3621
+ contextLoaderFactory: () => async (resource) => {
3622
+ const url = new URL(resource).href;
3623
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3624
+ if (url === remoteContextUrl) return {
3625
+ contextUrl: null,
3626
+ documentUrl: url,
3627
+ document: [
3628
+ "not",
3629
+ "an",
3630
+ "object"
3631
+ ]
3632
+ };
3633
+ throw new Error(`Unexpected context: ${url}`);
3634
+ }
3635
+ });
3636
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3637
+ throw new Error("listener should not run");
3638
+ }).onError(() => {
3639
+ errorCount++;
3640
+ });
3641
+ const signed = await signJsonLd({
3642
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3643
+ id: "https://remote.example/activities/legacy-invalid-remote-context",
3644
+ type: "Create",
3645
+ actor: "https://remote.example/users/alice",
3646
+ ext: "preserve-me",
3647
+ object: {
3648
+ id: "https://remote.example/notes/legacy-invalid-remote-context",
3649
+ type: "Note",
3650
+ attributedTo: "https://remote.example/users/alice",
3651
+ content: "Hello from invalid remote context queue"
3652
+ }
3653
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
3654
+ const url = new URL(resource).href;
3655
+ if (url === remoteContextUrl) return {
3656
+ contextUrl: null,
3657
+ documentUrl: url,
3658
+ document: { "@context": { ext: "https://example.com/ext" } }
3659
+ };
3660
+ return await mockDocumentLoader(url);
3661
+ } });
3662
+ const inboxMessage = {
3663
+ type: "inbox",
3664
+ id: crypto.randomUUID(),
3665
+ baseUrl: "https://example.com",
3666
+ activity: signed,
3667
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3668
+ attempt: 0,
3669
+ identifier: null,
3670
+ traceContext: {}
3671
+ };
3672
+ await federation.processQueuedTask(void 0, inboxMessage);
3673
+ assertEquals(errorCount, 1);
3674
+ assertEquals(queuedMessages, []);
3675
+ });
3676
+ await t.step("legacy raw LDS inbox messages with string remote contexts retry", async () => {
3677
+ const remoteContextUrl = "https://remote.example/contexts/ext";
3678
+ const queue = {
3679
+ enqueue(message, _options) {
3680
+ queuedMessages.push(message);
3681
+ return Promise.resolve();
3682
+ },
3683
+ listen(_handler, _options) {
3684
+ return Promise.resolve();
3685
+ }
3686
+ };
3687
+ const kv = new MemoryKvStore();
3688
+ const queuedMessages = [];
3689
+ let errorCount = 0;
3690
+ const federation = new FederationImpl({
3691
+ kv,
3692
+ queue,
3693
+ contextLoaderFactory: () => async (resource) => {
3694
+ const url = new URL(resource).href;
3695
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3696
+ if (url === remoteContextUrl) return {
3697
+ contextUrl: null,
3698
+ documentUrl: url,
3699
+ document: "{not valid json"
3700
+ };
3701
+ throw new Error(`Unexpected context: ${url}`);
3702
+ }
3703
+ });
3704
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3705
+ throw new Error("listener should not run");
3706
+ }).onError(() => {
3707
+ errorCount++;
3708
+ });
3709
+ const signed = await signJsonLd({
3710
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3711
+ id: "https://remote.example/activities/legacy-string-remote-context",
3712
+ type: "Create",
3713
+ actor: "https://remote.example/users/alice",
3714
+ ext: "preserve-me",
3715
+ object: {
3716
+ id: "https://remote.example/notes/legacy-string-remote-context",
3717
+ type: "Note",
3718
+ attributedTo: "https://remote.example/users/alice",
3719
+ content: "Hello from string remote context queue"
3720
+ }
3721
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
3722
+ const url = new URL(resource).href;
3723
+ if (url === remoteContextUrl) return {
3724
+ contextUrl: null,
3725
+ documentUrl: url,
3726
+ document: { "@context": { ext: "https://example.com/ext" } }
3727
+ };
3728
+ return await mockDocumentLoader(url);
3729
+ } });
3730
+ const inboxMessage = {
3731
+ type: "inbox",
3732
+ id: crypto.randomUUID(),
3733
+ baseUrl: "https://example.com",
3734
+ activity: signed,
3735
+ started: (/* @__PURE__ */ new Date()).toISOString(),
3736
+ attempt: 0,
3737
+ identifier: null,
3738
+ traceContext: {}
3739
+ };
3740
+ await federation.processQueuedTask(void 0, inboxMessage);
3741
+ assertEquals(errorCount, 1);
3742
+ assertEquals(queuedMessages, [{
3743
+ ...inboxMessage,
3744
+ attempt: 1
3745
+ }]);
3746
+ });
3747
+ await t.step("legacy raw LDS inbox messages with loader TypeErrors retry", async () => {
3748
+ const remoteContextUrl = "https://remote.example/contexts/ext";
3749
+ const queue = {
3750
+ enqueue(message, _options) {
3751
+ queuedMessages.push(message);
3752
+ return Promise.resolve();
3753
+ },
3754
+ listen(_handler, _options) {
3755
+ return Promise.resolve();
3756
+ }
3757
+ };
2345
3758
  const kv = new MemoryKvStore();
2346
- const [meterProvider, recorder] = createTestMeterProvider();
2347
- await new FederationImpl({
3759
+ const queuedMessages = [];
3760
+ let errorCount = 0;
3761
+ const federation = new FederationImpl({
2348
3762
  kv,
2349
- meterProvider,
2350
- queue: {
2351
- enqueue(_message, _options) {
2352
- return Promise.resolve();
2353
- },
2354
- listen(_handler, _options) {
2355
- return Promise.resolve();
2356
- }
2357
- },
2358
- outboxRetryPolicy: () => null
2359
- }).processQueuedTask(void 0, {
2360
- type: "outbox",
3763
+ queue,
3764
+ contextLoaderFactory: () => async (resource) => {
3765
+ const url = new URL(resource).href;
3766
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3767
+ if (url === remoteContextUrl) throw new TypeError(`Cannot initialize remote context loader: ${url}`);
3768
+ throw new Error(`Unexpected context: ${url}`);
3769
+ }
3770
+ });
3771
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3772
+ throw new Error("listener should not run");
3773
+ }).onError(() => {
3774
+ errorCount++;
3775
+ });
3776
+ const signed = await signJsonLd({
3777
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3778
+ id: "https://remote.example/activities/legacy-typeerror-context",
3779
+ type: "Create",
3780
+ actor: "https://remote.example/users/alice",
3781
+ ext: "preserve-me",
3782
+ object: {
3783
+ id: "https://remote.example/notes/legacy-typeerror-context",
3784
+ type: "Note",
3785
+ attributedTo: "https://remote.example/users/alice",
3786
+ content: "Hello from typeerror legacy queue"
3787
+ }
3788
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
3789
+ const url = new URL(resource).href;
3790
+ if (url === remoteContextUrl) return {
3791
+ contextUrl: null,
3792
+ documentUrl: url,
3793
+ document: { "@context": { ext: "https://example.com/ext" } }
3794
+ };
3795
+ return await mockDocumentLoader(url);
3796
+ } });
3797
+ const inboxMessage = {
3798
+ type: "inbox",
2361
3799
  id: crypto.randomUUID(),
2362
3800
  baseUrl: "https://example.com",
2363
- keys: [],
2364
- activity: {
2365
- "@context": "https://www.w3.org/ns/activitystreams",
2366
- type: "Follow",
2367
- actor: "https://example.com/users/alice",
2368
- object: "https://remote.example/users/bob"
2369
- },
2370
- activityType: "https://www.w3.org/ns/activitystreams#Follow",
2371
- inbox: "https://invalid-domain-that-does-not-exist.example/inbox",
2372
- sharedInbox: false,
3801
+ activity: signed,
2373
3802
  started: (/* @__PURE__ */ new Date()).toISOString(),
2374
3803
  attempt: 0,
2375
- headers: {},
3804
+ identifier: null,
2376
3805
  traceContext: {}
2377
- });
2378
- const outboxLifecycle = recorder.getMeasurements("activitypub.outbox.activity");
2379
- assertEquals(outboxLifecycle.length, 1);
2380
- assertEquals(outboxLifecycle[0].type, "counter");
2381
- assertEquals(outboxLifecycle[0].attributes["activitypub.processing.result"], "abandoned");
2382
- assertEquals(outboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Follow");
3806
+ };
3807
+ await federation.processQueuedTask(void 0, inboxMessage);
3808
+ assertEquals(errorCount, 1);
3809
+ assertEquals(queuedMessages, [{
3810
+ ...inboxMessage,
3811
+ attempt: 1
3812
+ }]);
2383
3813
  });
2384
- await t.step("records activitypub.inbox.activity processed on successful queued dispatch", async () => {
3814
+ await t.step("legacy raw LDS inbox messages with syntax errors in remote contexts retry", async () => {
3815
+ const remoteContextUrl = "https://remote.example/contexts/ext";
3816
+ const queue = {
3817
+ enqueue(message, _options) {
3818
+ queuedMessages.push(message);
3819
+ return Promise.resolve();
3820
+ },
3821
+ listen(_handler, _options) {
3822
+ return Promise.resolve();
3823
+ }
3824
+ };
2385
3825
  const kv = new MemoryKvStore();
2386
- const [meterProvider, recorder] = createTestMeterProvider();
3826
+ const queuedMessages = [];
3827
+ let errorCount = 0;
2387
3828
  const federation = new FederationImpl({
2388
3829
  kv,
2389
- meterProvider,
2390
- queue: {
2391
- enqueue(_message, _options) {
2392
- return Promise.resolve();
2393
- },
2394
- listen(_handler, _options) {
2395
- return Promise.resolve();
3830
+ queue,
3831
+ contextLoaderFactory: () => async (resource) => {
3832
+ const url = new URL(resource).href;
3833
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3834
+ if (url === remoteContextUrl) {
3835
+ const error = /* @__PURE__ */ new Error(`Transient syntax failure: ${url}`);
3836
+ error.name = "jsonld.SyntaxError";
3837
+ error.details = { code: "loading remote context failed" };
3838
+ throw error;
2396
3839
  }
3840
+ throw new Error(`Unexpected context: ${url}`);
2397
3841
  }
2398
3842
  });
2399
- federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {});
2400
- await federation.processQueuedTask(void 0, {
3843
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3844
+ throw new Error("listener should not run");
3845
+ }).onError(() => {
3846
+ errorCount++;
3847
+ });
3848
+ const signed = await signJsonLd({
3849
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3850
+ id: "https://remote.example/activities/legacy-syntax-context",
3851
+ type: "Create",
3852
+ actor: "https://remote.example/users/alice",
3853
+ ext: "preserve-me",
3854
+ object: {
3855
+ id: "https://remote.example/notes/legacy-syntax-context",
3856
+ type: "Note",
3857
+ attributedTo: "https://remote.example/users/alice",
3858
+ content: "Hello from syntax legacy queue"
3859
+ }
3860
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
3861
+ const url = new URL(resource).href;
3862
+ if (url === remoteContextUrl) return {
3863
+ contextUrl: null,
3864
+ documentUrl: url,
3865
+ document: { "@context": { ext: "https://example.com/ext" } }
3866
+ };
3867
+ return await mockDocumentLoader(url);
3868
+ } });
3869
+ const inboxMessage = {
2401
3870
  type: "inbox",
2402
3871
  id: crypto.randomUUID(),
2403
3872
  baseUrl: "https://example.com",
2404
- activity: {
2405
- "@context": "https://www.w3.org/ns/activitystreams",
2406
- type: "Create",
2407
- id: "https://example.com/activities/queued-processed",
2408
- actor: "https://remote.example/users/alice",
2409
- object: {
2410
- type: "Note",
2411
- content: "Hello world"
2412
- }
2413
- },
3873
+ activity: signed,
2414
3874
  started: (/* @__PURE__ */ new Date()).toISOString(),
2415
3875
  attempt: 0,
2416
3876
  identifier: null,
2417
3877
  traceContext: {}
2418
- });
2419
- const inboxLifecycle = recorder.getMeasurements("activitypub.inbox.activity");
2420
- assertEquals(inboxLifecycle.length, 1);
2421
- assertEquals(inboxLifecycle[0].type, "counter");
2422
- assertEquals(inboxLifecycle[0].attributes["activitypub.processing.result"], "processed");
2423
- assertEquals(inboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
3878
+ };
3879
+ await federation.processQueuedTask(void 0, inboxMessage);
3880
+ assertEquals(errorCount, 1);
3881
+ assertEquals(queuedMessages, [{
3882
+ ...inboxMessage,
3883
+ attempt: 1
3884
+ }]);
2424
3885
  });
2425
- await t.step("records activitypub.inbox.activity retried on transient listener failure", async () => {
3886
+ await t.step("legacy raw LDS inbox messages with loader RangeErrors retry", async () => {
3887
+ const remoteContextUrl = "https://remote.example/contexts/ext";
3888
+ const queue = {
3889
+ enqueue(message, _options) {
3890
+ queuedMessages.push(message);
3891
+ return Promise.resolve();
3892
+ },
3893
+ listen(_handler, _options) {
3894
+ return Promise.resolve();
3895
+ }
3896
+ };
2426
3897
  const kv = new MemoryKvStore();
2427
- const [meterProvider, recorder] = createTestMeterProvider();
3898
+ const queuedMessages = [];
3899
+ let errorCount = 0;
2428
3900
  const federation = new FederationImpl({
2429
3901
  kv,
2430
- meterProvider,
2431
- queue: {
2432
- enqueue(_message, _options) {
2433
- return Promise.resolve();
2434
- },
2435
- listen(_handler, _options) {
2436
- return Promise.resolve();
2437
- }
3902
+ queue,
3903
+ contextLoaderFactory: () => async (resource) => {
3904
+ const url = new URL(resource).href;
3905
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
3906
+ if (url === remoteContextUrl) throw new RangeError(`Temporary remote context cache window exceeded: ${url}`);
3907
+ throw new Error(`Unexpected context: ${url}`);
2438
3908
  }
2439
3909
  });
2440
- federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {
2441
- throw new Error("Intended error for testing");
2442
- });
2443
- await federation.processQueuedTask(void 0, {
3910
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3911
+ throw new Error("listener should not run");
3912
+ }).onError(() => {
3913
+ errorCount++;
3914
+ });
3915
+ const signed = await signJsonLd({
3916
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
3917
+ id: "https://remote.example/activities/legacy-rangeerror-context",
3918
+ type: "Create",
3919
+ actor: "https://remote.example/users/alice",
3920
+ ext: "preserve-me",
3921
+ object: {
3922
+ id: "https://remote.example/notes/legacy-rangeerror-context",
3923
+ type: "Note",
3924
+ attributedTo: "https://remote.example/users/alice",
3925
+ content: "Hello from rangeerror legacy queue"
3926
+ }
3927
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
3928
+ const url = new URL(resource).href;
3929
+ if (url === remoteContextUrl) return {
3930
+ contextUrl: null,
3931
+ documentUrl: url,
3932
+ document: { "@context": { ext: "https://example.com/ext" } }
3933
+ };
3934
+ return await mockDocumentLoader(url);
3935
+ } });
3936
+ const inboxMessage = {
2444
3937
  type: "inbox",
2445
3938
  id: crypto.randomUUID(),
2446
3939
  baseUrl: "https://example.com",
2447
- activity: {
2448
- "@context": "https://www.w3.org/ns/activitystreams",
2449
- type: "Create",
2450
- id: "https://example.com/activities/queued-retried",
2451
- actor: "https://remote.example/users/alice",
2452
- object: {
2453
- type: "Note",
2454
- content: "Hello world"
2455
- }
2456
- },
3940
+ activity: signed,
2457
3941
  started: (/* @__PURE__ */ new Date()).toISOString(),
2458
3942
  attempt: 0,
2459
3943
  identifier: null,
2460
3944
  traceContext: {}
2461
- });
2462
- const inboxLifecycle = recorder.getMeasurements("activitypub.inbox.activity");
2463
- assertEquals(inboxLifecycle.length, 1);
2464
- assertEquals(inboxLifecycle[0].attributes["activitypub.processing.result"], "retried");
2465
- assertEquals(inboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
3945
+ };
3946
+ await federation.processQueuedTask(void 0, inboxMessage);
3947
+ assertEquals(errorCount, 1);
3948
+ assertEquals(queuedMessages, [{
3949
+ ...inboxMessage,
3950
+ attempt: 1
3951
+ }]);
2466
3952
  });
2467
- await t.step("records activitypub.inbox.activity abandoned when retry policy gives up", async () => {
3953
+ await t.step("permanent queued inbox parse errors do not re-enqueue poison messages", async () => {
3954
+ const queuedMessages = [];
3955
+ const queue = {
3956
+ enqueue(message, _options) {
3957
+ queuedMessages.push(message);
3958
+ return Promise.resolve();
3959
+ },
3960
+ listen(_handler, _options) {
3961
+ return Promise.resolve();
3962
+ }
3963
+ };
2468
3964
  const kv = new MemoryKvStore();
2469
- const [meterProvider, recorder] = createTestMeterProvider();
3965
+ let errorCount = 0;
2470
3966
  const federation = new FederationImpl({
2471
3967
  kv,
2472
- meterProvider,
2473
- queue: {
2474
- enqueue(_message, _options) {
2475
- return Promise.resolve();
2476
- },
2477
- listen(_handler, _options) {
2478
- return Promise.resolve();
2479
- }
2480
- },
2481
- inboxRetryPolicy: () => null
3968
+ queue
2482
3969
  });
2483
- federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {
2484
- throw new Error("Intended error for testing");
3970
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
3971
+ throw new Error("listener should not run");
3972
+ }).onError(() => {
3973
+ errorCount++;
2485
3974
  });
2486
3975
  await federation.processQueuedTask(void 0, {
2487
3976
  type: "inbox",
@@ -2489,55 +3978,65 @@ test("FederationImpl.processQueuedTask()", async (t) => {
2489
3978
  baseUrl: "https://example.com",
2490
3979
  activity: {
2491
3980
  "@context": "https://www.w3.org/ns/activitystreams",
2492
- type: "Create",
2493
- id: "https://example.com/activities/queued-abandoned",
2494
- actor: "https://remote.example/users/alice",
2495
- object: {
2496
- type: "Note",
2497
- content: "Hello world"
2498
- }
3981
+ id: "https://remote.example/objects/not-an-activity",
3982
+ type: "Note",
3983
+ attributedTo: "https://remote.example/users/alice",
3984
+ content: "Not an activity"
2499
3985
  },
2500
3986
  started: (/* @__PURE__ */ new Date()).toISOString(),
2501
3987
  attempt: 0,
2502
3988
  identifier: null,
2503
3989
  traceContext: {}
2504
3990
  });
2505
- const inboxLifecycle = recorder.getMeasurements("activitypub.inbox.activity");
2506
- assertEquals(inboxLifecycle.length, 1);
2507
- assertEquals(inboxLifecycle[0].attributes["activitypub.processing.result"], "abandoned");
2508
- assertEquals(inboxLifecycle[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
3991
+ assertEquals(errorCount, 1);
3992
+ assertEquals(queuedMessages, []);
2509
3993
  });
2510
- await t.step("records queued inbox processing duration", async () => {
3994
+ await t.step("malformed Temporal fields are permanent queued inbox parse errors", async () => {
3995
+ const queuedMessages = [];
3996
+ const queue = {
3997
+ enqueue(message, _options) {
3998
+ queuedMessages.push(message);
3999
+ return Promise.resolve();
4000
+ },
4001
+ listen(_handler, _options) {
4002
+ return Promise.resolve();
4003
+ }
4004
+ };
2511
4005
  const kv = new MemoryKvStore();
2512
- const [meterProvider, recorder] = createTestMeterProvider();
4006
+ let errorCount = 0;
2513
4007
  const federation = new FederationImpl({
2514
4008
  kv,
2515
- meterProvider,
2516
- queue: {
2517
- enqueue(_message, _options) {
2518
- return Promise.resolve();
2519
- },
2520
- listen(_handler, _options) {
2521
- return Promise.resolve();
2522
- }
2523
- }
4009
+ queue,
4010
+ documentLoaderFactory: () => mockDocumentLoader,
4011
+ contextLoaderFactory: () => mockDocumentLoader
2524
4012
  });
2525
- let handled = false;
2526
- federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, () => {
2527
- handled = true;
4013
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
4014
+ throw new Error("listener should not run");
4015
+ }).onError(() => {
4016
+ errorCount++;
2528
4017
  });
2529
4018
  await federation.processQueuedTask(void 0, {
2530
4019
  type: "inbox",
2531
4020
  id: crypto.randomUUID(),
2532
4021
  baseUrl: "https://example.com",
2533
4022
  activity: {
2534
- "@context": "https://www.w3.org/ns/activitystreams",
4023
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"],
4024
+ id: "https://remote.example/activities/invalid-proof-created",
2535
4025
  type: "Create",
2536
- id: "https://remote.example/activities/1",
2537
4026
  actor: "https://remote.example/users/alice",
2538
4027
  object: {
4028
+ id: "https://remote.example/notes/invalid-proof-created",
2539
4029
  type: "Note",
2540
- content: "Hello world"
4030
+ attributedTo: "https://remote.example/users/alice",
4031
+ content: "Hello, world!"
4032
+ },
4033
+ proof: {
4034
+ type: "DataIntegrityProof",
4035
+ cryptosuite: "eddsa-jcs-2022",
4036
+ verificationMethod: "https://remote.example/users/alice#ed25519-key",
4037
+ proofPurpose: "assertionMethod",
4038
+ created: { "@value": "not-a-date" },
4039
+ proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z"
2541
4040
  }
2542
4041
  },
2543
4042
  started: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2545,32 +4044,8 @@ test("FederationImpl.processQueuedTask()", async (t) => {
2545
4044
  identifier: null,
2546
4045
  traceContext: {}
2547
4046
  });
2548
- assert(handled);
2549
- const durations = recorder.getMeasurements("activitypub.inbox.processing_duration");
2550
- assertEquals(durations.length, 1);
2551
- assertEquals(durations[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2552
- const started = recorder.getMeasurements("fedify.queue.task.started");
2553
- assertEquals(started.length, 1);
2554
- assertEquals(started[0].attributes["fedify.queue.role"], "inbox");
2555
- const completed = recorder.getMeasurements("fedify.queue.task.completed");
2556
- assertEquals(completed.length, 1);
2557
- assertEquals(completed[0].attributes["fedify.queue.role"], "inbox");
2558
- assertEquals(completed[0].attributes["fedify.queue.task.result"], "completed");
2559
- assertEquals(completed[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
2560
- assertEquals(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
2561
- const taskDurations = recorder.getMeasurements("fedify.queue.task.duration");
2562
- assertEquals(taskDurations.length, 1);
2563
- assertEquals(taskDurations[0].type, "histogram");
2564
- assertEquals(taskDurations[0].attributes["fedify.queue.role"], "inbox");
2565
- assertEquals(taskDurations[0].attributes["fedify.queue.task.result"], "completed");
2566
- const inFlight = recorder.getMeasurements("fedify.queue.task.in_flight");
2567
- assertEquals(inFlight.length, 2);
2568
- assertEquals(inFlight[0].type, "upDownCounter");
2569
- assertEquals(inFlight[0].value, 1);
2570
- assertEquals(inFlight[1].value, -1);
2571
- assertEquals(inFlight[0].attributes, inFlight[1].attributes);
2572
- assertEquals(inFlight[0].attributes["fedify.queue.role"], "inbox");
2573
- assertEquals(inFlight[0].attributes["activitypub.activity.type"], void 0);
4047
+ assertEquals(errorCount, 1);
4048
+ assertEquals(queuedMessages, []);
2574
4049
  });
2575
4050
  });
2576
4051
  test("FederationImpl.processQueuedTask() permanent failure", async (t) => {
@@ -3917,6 +5392,35 @@ test({
3917
5392
  assertEquals(rejected[1].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Offer");
3918
5393
  }
3919
5394
  });
5395
+ test("ContextImpl.routeActivity() marks queued signed activities as non-LDS", async () => {
5396
+ let queuedMessage = null;
5397
+ const federation = new FederationImpl({
5398
+ kv: new MemoryKvStore(),
5399
+ queue: {
5400
+ enqueue(message) {
5401
+ queuedMessage = message;
5402
+ return Promise.resolve();
5403
+ },
5404
+ async listen() {}
5405
+ }
5406
+ });
5407
+ federation.setInboxListeners("/u/{identifier}/i", "/i").on(Offer, () => {
5408
+ throw new Error("listener should not run for queued routeActivity");
5409
+ });
5410
+ const ctx = new ContextImpl({
5411
+ url: new URL("https://example.com/"),
5412
+ federation,
5413
+ data: void 0,
5414
+ documentLoader: mockDocumentLoader,
5415
+ contextLoader: documentLoader
5416
+ });
5417
+ const signedOffer = await signObject(new Offer({ actor: new URL("https://example.com/person2") }), ed25519PrivateKey, ed25519Multikey.id);
5418
+ assert(await ctx.routeActivity(null, signedOffer));
5419
+ if (queuedMessage == null) throw new Error("Inbox message not queued.");
5420
+ const inboxMessage = queuedMessage;
5421
+ assertEquals(inboxMessage.ldSignatureVerified, false);
5422
+ assertEquals(inboxMessage.normalizedActivity, void 0);
5423
+ });
3920
5424
  test("ContextImpl.getCollectionUri()", () => {
3921
5425
  const federation = new FederationImpl({ kv: new MemoryKvStore() });
3922
5426
  const base = "https://example.com";