@fedify/fedify 2.3.0-dev.1145 → 2.3.0-dev.1150
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.
- package/dist/{builder-ShiR1K6b.mjs → builder-Bjm1Jq9n.mjs} +2 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-DI2gRbyN.d.cts → context-CRXCkTM6.d.cts} +48 -6
- package/dist/{context-DCtsSHDv.d.ts → context-MgCh7YGu.d.ts} +48 -6
- package/dist/{deno-h0TWFuEz.mjs → deno-CKFE6Uya.mjs} +1 -1
- package/dist/{docloader-BdDN0Aqx.mjs → docloader-B-ZE1cZf.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +1363 -44
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +1667 -163
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/retry.test.mjs +1 -1
- package/dist/federation/send.test.mjs +8 -8
- package/dist/federation/temporal.test.d.mts +2 -0
- package/dist/federation/temporal.test.mjs +71 -0
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{getMachineId-bsd-etIyxDet.mjs → getMachineId-bsd-BY01PL1n.mjs} +1 -1
- package/dist/{getMachineId-darwin-D23zTf4g.mjs → getMachineId-darwin-Dr1gkBkp.mjs} +1 -1
- package/dist/{getMachineId-win-Dpap6v5i.mjs → getMachineId-win-QEYwcJiy.mjs} +1 -1
- package/dist/{http-7kAB7PVx.cjs → http-DQYEA7AZ.cjs} +1 -1
- package/dist/{http-B2hxA7dO.js → http-WbS1gKzr.js} +1 -1
- package/dist/{http-QzW9IWfs.mjs → http-vHCgbhTg.mjs} +3 -3
- package/dist/{key-Dh2OK1XQ.mjs → key-N0zP_oJA.mjs} +2 -2
- package/dist/{kv-cache-b22dNkjt.js → kv-cache-DM2O-Yjy.js} +1 -1
- package/dist/{kv-cache-DCPp-MT0.cjs → kv-cache-Dsg_bi4N.cjs} +1 -1
- package/dist/{kv-cache-EZRIPZXD.mjs → kv-cache-GXXZEemD.mjs} +1 -1
- package/dist/{ld-eZbar1rr.mjs → ld-BwKhquPx.mjs} +302 -6
- package/dist/{metrics-E0hAHtLZ.mjs → metrics-7Vy9FvEw.mjs} +1 -1
- package/dist/{middleware-BrGIM_Ra.js → middleware-BscgvU-m.js} +428 -99
- package/dist/{middleware-BUl1BH4x.cjs → middleware-D_iXrYHJ.cjs} +429 -99
- package/dist/{middleware-mToCR2tG.mjs → middleware-Db1_qAFG.mjs} +1 -1
- package/dist/{middleware-CyJDCmNg.mjs → middleware-ZuUcO0t1.mjs} +348 -108
- package/dist/{mod-CI9fduEi.d.cts → mod-C7HOzGqH.d.cts} +1 -1
- package/dist/{mod-CkRiJHGA.d.ts → mod-CpQHB3Ys.d.ts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +2 -2
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-ByO_Fw6U.mjs → owner-FD0H_vpj.mjs} +2 -2
- package/dist/{proof-jVqClF49.cjs → proof-CYK8T8IS.cjs} +353 -3
- package/dist/{proof-BkRyFchv.js → proof-I3EokKN-.js} +300 -4
- package/dist/{proof-CSo0S8OK.mjs → proof-V_lafPmA.mjs} +3 -3
- package/dist/{send-jzrTV1FU.mjs → send-Cc2_10tF.mjs} +3 -3
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +558 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +1 -1
- package/dist/temporal-BkmBfs__.mjs +95 -0
- package/dist/testing/mod.d.mts +48 -6
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
- /package/dist/{execAsync-DCBrgFiV.mjs → execAsync-Dxb7rNf3.mjs} +0 -0
- /package/dist/{getMachineId-linux-ObI47Hql.mjs → getMachineId-linux-Bbhofx-s.mjs} +0 -0
- /package/dist/{getMachineId-unsupported-Ddu-PFeh.mjs → getMachineId-unsupported-dIOte2Ct.mjs} +0 -0
- /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-
|
|
13
|
+
import { l as verifyRequest, s as signRequest } from "../http-vHCgbhTg.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-
|
|
16
|
-
import { a as
|
|
17
|
-
import { t as doesActorOwnKey } from "../owner-
|
|
18
|
-
import { i as verifyObject, r as signObject } from "../proof-
|
|
15
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-B-ZE1cZf.mjs";
|
|
16
|
+
import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-BwKhquPx.mjs";
|
|
17
|
+
import { t as doesActorOwnKey } from "../owner-FD0H_vpj.mjs";
|
|
18
|
+
import { i as verifyObject, r as signObject } from "../proof-V_lafPmA.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-
|
|
20
|
+
import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-ZuUcO0t1.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
|
|
2347
|
-
|
|
3759
|
+
const queuedMessages = [];
|
|
3760
|
+
let errorCount = 0;
|
|
3761
|
+
const federation = new FederationImpl({
|
|
2348
3762
|
kv,
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
}).
|
|
2360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3804
|
+
identifier: null,
|
|
2376
3805
|
traceContext: {}
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
assertEquals(
|
|
2380
|
-
assertEquals(
|
|
2381
|
-
|
|
2382
|
-
|
|
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("
|
|
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
|
|
3826
|
+
const queuedMessages = [];
|
|
3827
|
+
let errorCount = 0;
|
|
2387
3828
|
const federation = new FederationImpl({
|
|
2388
3829
|
kv,
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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(
|
|
2400
|
-
|
|
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
|
-
|
|
2420
|
-
assertEquals(
|
|
2421
|
-
assertEquals(
|
|
2422
|
-
|
|
2423
|
-
|
|
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("
|
|
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
|
|
3898
|
+
const queuedMessages = [];
|
|
3899
|
+
let errorCount = 0;
|
|
2428
3900
|
const federation = new FederationImpl({
|
|
2429
3901
|
kv,
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
}
|
|
2435
|
-
|
|
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(
|
|
2441
|
-
throw new Error("
|
|
2442
|
-
})
|
|
2443
|
-
|
|
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
|
-
|
|
2463
|
-
assertEquals(
|
|
2464
|
-
assertEquals(
|
|
2465
|
-
|
|
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("
|
|
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
|
-
|
|
3965
|
+
let errorCount = 0;
|
|
2470
3966
|
const federation = new FederationImpl({
|
|
2471
3967
|
kv,
|
|
2472
|
-
|
|
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(
|
|
2484
|
-
throw new Error("
|
|
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
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
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
|
-
|
|
2506
|
-
assertEquals(
|
|
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("
|
|
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
|
-
|
|
4006
|
+
let errorCount = 0;
|
|
2513
4007
|
const federation = new FederationImpl({
|
|
2514
4008
|
kv,
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2549
|
-
|
|
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";
|