@fedify/fedify 2.2.18-dev.2386 → 2.2.18
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/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-WyYSTDDX.mjs} +2 -20
- package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-D4DNWrux.mjs} +19 -1
- package/dist/{builder-CiKYvR0I.mjs → builder-DAbQyxmb.mjs} +2 -2
- package/dist/compat/public-audience.test.mjs +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{deno-CkabM5bf.mjs → deno-JUTr6LYC.mjs} +1 -1
- package/dist/{docloader-B4Hk3LSj.mjs → docloader-BGjq1F5U.mjs} +4 -2
- package/dist/federation/builder.test.mjs +3 -3
- package/dist/federation/collection.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +261 -8
- package/dist/federation/idempotency.test.mjs +3 -3
- package/dist/federation/inbox.test.mjs +25 -1
- package/dist/federation/kv.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +128 -10
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +2 -2
- package/dist/federation/negotiation.test.mjs +3 -3
- package/dist/federation/retry.test.mjs +2 -2
- package/dist/federation/router.test.mjs +2 -3
- package/dist/federation/send.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +3 -3
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{http-C_Qq0cNx.cjs → http-DEH_eppX.cjs} +1 -1
- package/dist/{http-DP-vbG4o.js → http-OSYYhKM8.js} +1 -1
- package/dist/{http-DovhI1VM.mjs → http-lCmKCZBI.mjs} +2 -2
- package/dist/inbox-DMATfdEI.mjs +162 -0
- package/dist/{key-BtVvc9pe.mjs → key-D0ahOVjf.mjs} +1 -1
- package/dist/{kv-cache-D8yJIXRo.js → kv-cache-B7X4p1y7.js} +3 -1
- package/dist/{kv-cache-Ds_adaor.cjs → kv-cache-CPEV_4nZ.cjs} +3 -1
- package/dist/{ld-1J9cdTp0.mjs → ld-CJOXIehz.mjs} +2 -2
- package/dist/{middleware-DMkt5ShX.mjs → middleware-BgGJEcdp.mjs} +1 -1
- package/dist/{middleware-ruFPV2xR.mjs → middleware-CHoUjmJm.mjs} +84 -167
- package/dist/{middleware-B_M6lcay.cjs → middleware-D3hx1aiI.cjs} +1 -1
- package/dist/{middleware-BWXhlsuq.cjs → middleware-QcuVRA-q.cjs} +96 -26
- package/dist/{middleware-BrFMyQSp.js → middleware-aNUajYPY.js} +95 -25
- package/dist/mod.cjs +4 -4
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/client.test.mjs +1 -1
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/nodeinfo/types.test.mjs +2 -2
- package/dist/otel/exporter.test.mjs +1 -1
- package/dist/{owner-CgurOfJ9.mjs → owner-BUTMEkbn.mjs} +2 -2
- package/dist/{proof-C6uAR-Hj.cjs → proof-BeY7TsiS.cjs} +1 -1
- package/dist/{proof-BJ332wO_.js → proof-Bm3xyn3D.js} +1 -1
- package/dist/{proof-Dn4nm7pr.mjs → proof-DS1Zl6BX.mjs} +2 -2
- package/dist/{send-iVwnK504.mjs → send-fnPrh9Zm.mjs} +2 -2
- package/dist/sig/http.test.mjs +5 -5
- package/dist/sig/key.test.mjs +5 -5
- package/dist/sig/ld.test.mjs +4 -4
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +3 -3
- package/dist/sig/proof.test.mjs +4 -3
- package/dist/{std__assert-BVFj8-_V.mjs → std__assert-DNZ4Liil.mjs} +2 -2
- package/dist/{temporal-BZrzDMq2.mjs → temporal-Biz6zK6e.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +114 -39
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +5 -5
- /package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-BkxW5QJI.mjs} +0 -0
- /package/dist/{negotiation-BHtZN6Y1.mjs → negotiation-SQvQgUqe.mjs} +0 -0
- /package/dist/{retry-bMXBL97A.mjs → retry-v_sGLH1d.mjs} +0 -0
|
@@ -2,25 +2,7 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { l as AssertionError } from "./assert_equals-Ew3jOFa3.mjs";
|
|
5
|
-
import { n as assertIsError } from "./assert_throws-
|
|
6
|
-
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_false.js
|
|
7
|
-
/**
|
|
8
|
-
* Make an assertion, error will be thrown if `expr` have truthy value.
|
|
9
|
-
*
|
|
10
|
-
* @example Usage
|
|
11
|
-
* ```ts no-eval
|
|
12
|
-
* import { assertFalse } from "@std/assert/assert-false";
|
|
13
|
-
*
|
|
14
|
-
* assertFalse(false); // Doesn't throw
|
|
15
|
-
* assertFalse(true); // Throws
|
|
16
|
-
* ```
|
|
17
|
-
*
|
|
18
|
-
* @param expr The expression to test.
|
|
19
|
-
* @param msg The optional message to display if the assertion fails.
|
|
20
|
-
*/ function assertFalse(expr, msg = "") {
|
|
21
|
-
if (expr) throw new AssertionError(msg);
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
5
|
+
import { n as assertIsError } from "./assert_throws-D4DNWrux.mjs";
|
|
24
6
|
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_rejects.js
|
|
25
7
|
async function assertRejects(fn, errorClassOrMsg, msgIncludesOrMsg, msg) {
|
|
26
8
|
let ErrorClass = void 0;
|
|
@@ -54,4 +36,4 @@ async function assertRejects(fn, errorClassOrMsg, msgIncludesOrMsg, msg) {
|
|
|
54
36
|
return err;
|
|
55
37
|
}
|
|
56
38
|
//#endregion
|
|
57
|
-
export {
|
|
39
|
+
export { assertRejects as t };
|
|
@@ -2,6 +2,24 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { l as AssertionError, o as stripAnsiCode } from "./assert_equals-Ew3jOFa3.mjs";
|
|
5
|
+
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_false.js
|
|
6
|
+
/**
|
|
7
|
+
* Make an assertion, error will be thrown if `expr` have truthy value.
|
|
8
|
+
*
|
|
9
|
+
* @example Usage
|
|
10
|
+
* ```ts no-eval
|
|
11
|
+
* import { assertFalse } from "@std/assert/assert-false";
|
|
12
|
+
*
|
|
13
|
+
* assertFalse(false); // Doesn't throw
|
|
14
|
+
* assertFalse(true); // Throws
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @param expr The expression to test.
|
|
18
|
+
* @param msg The optional message to display if the assertion fails.
|
|
19
|
+
*/ function assertFalse(expr, msg = "") {
|
|
20
|
+
if (expr) throw new AssertionError(msg);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
5
23
|
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_is_error.js
|
|
6
24
|
/**
|
|
7
25
|
* Make an assertion that `error` is an `Error`.
|
|
@@ -70,4 +88,4 @@ function assertThrows(fn, errorClassOrMsg, msgIncludesOrMsg, msg) {
|
|
|
70
88
|
return err;
|
|
71
89
|
}
|
|
72
90
|
//#endregion
|
|
73
|
-
export { assertIsError as n, assertThrows as t };
|
|
91
|
+
export { assertIsError as n, assertFalse as r, assertThrows as t };
|
|
@@ -2,7 +2,7 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { n as RouterError, t as Router } from "./router-CrMLXoOr.mjs";
|
|
5
|
-
import { n as version, t as name } from "./deno-
|
|
5
|
+
import { n as version, t as name } from "./deno-JUTr6LYC.mjs";
|
|
6
6
|
import { t as ActivityListenerSet } from "./activity-listener-ell7W1s9.mjs";
|
|
7
7
|
import { Tombstone, getTypeId } from "@fedify/vocab";
|
|
8
8
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
@@ -59,7 +59,7 @@ var FederationBuilderImpl = class {
|
|
|
59
59
|
this.collectionTypeIds = {};
|
|
60
60
|
}
|
|
61
61
|
async build(options) {
|
|
62
|
-
const { FederationImpl } = await import("./middleware-
|
|
62
|
+
const { FederationImpl } = await import("./middleware-BgGJEcdp.mjs");
|
|
63
63
|
const f = new FederationImpl(options);
|
|
64
64
|
const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
|
|
65
65
|
f.router = this.router.clone();
|
|
@@ -2,7 +2,7 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
|
-
import { t as assertNotEquals } from "../assert_not_equals
|
|
5
|
+
import { t as assertNotEquals } from "../assert_not_equals-BkxW5QJI.mjs";
|
|
6
6
|
import { t as normalizePublicAudience } from "../public-audience-DpJqzPuh.mjs";
|
|
7
7
|
import { test } from "@fedify/fixture";
|
|
8
8
|
import { Create, Follow, Note, PUBLIC_COLLECTION } from "@fedify/vocab";
|
|
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
|
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
5
|
import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
|
|
6
6
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
7
|
-
import { n as FederationImpl, v as actorDehydrator, y as autoIdAssigner } from "../middleware-
|
|
7
|
+
import { n as FederationImpl, v as actorDehydrator, y as autoIdAssigner } from "../middleware-CHoUjmJm.mjs";
|
|
8
8
|
import { test } from "@fedify/fixture";
|
|
9
9
|
import { Follow, Person } from "@fedify/vocab";
|
|
10
10
|
//#region src/compat/transformers.test.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { s as validateCryptoKey } from "./key-
|
|
5
|
-
import { n as doubleKnock } from "./http-
|
|
4
|
+
import { s as validateCryptoKey } from "./key-D0ahOVjf.mjs";
|
|
5
|
+
import { n as doubleKnock } from "./http-lCmKCZBI.mjs";
|
|
6
6
|
import { curry } from "es-toolkit";
|
|
7
7
|
import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
@@ -59,6 +59,8 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
async function validateUrl(url) {
|
|
62
|
+
const protocol = new URL(url).protocol;
|
|
63
|
+
if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
|
|
62
64
|
if (!allowPrivateAddress) try {
|
|
63
65
|
await validatePublicUrl(url);
|
|
64
66
|
} catch (error) {
|
|
@@ -3,11 +3,11 @@ import "urlpattern-polyfill";
|
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { n as RouterError } from "../router-CrMLXoOr.mjs";
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
6
|
-
import { r as assertExists } from "../std__assert-
|
|
7
|
-
import { t as assertThrows } from "../assert_throws-
|
|
6
|
+
import { r as assertExists } from "../std__assert-DNZ4Liil.mjs";
|
|
7
|
+
import { t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
|
|
8
8
|
import { t as assertStrictEquals } from "../assert_strict_equals-Dmjbg-bA.mjs";
|
|
9
9
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
10
|
-
import { n as createFederationBuilder } from "../builder-
|
|
10
|
+
import { n as createFederationBuilder } from "../builder-DAbQyxmb.mjs";
|
|
11
11
|
import { test } from "@fedify/fixture";
|
|
12
12
|
import { Activity, Note, Person } from "@fedify/vocab";
|
|
13
13
|
//#region src/federation/builder.test.ts
|
|
@@ -2,7 +2,7 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
|
-
import "../std__assert-
|
|
5
|
+
import "../std__assert-DNZ4Liil.mjs";
|
|
6
6
|
import { n as digest, t as buildCollectionSynchronizationHeader } from "../collection-D-HqUuA2.mjs";
|
|
7
7
|
import { test } from "@fedify/fixture";
|
|
8
8
|
import { decodeHex } from "byte-encodings/hex";
|
|
@@ -3,17 +3,17 @@ import "urlpattern-polyfill";
|
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-Dk_tacqz.mjs";
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
6
|
-
import "../std__assert-
|
|
7
|
-
import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
|
|
6
|
+
import "../std__assert-DNZ4Liil.mjs";
|
|
8
7
|
import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
|
|
8
|
+
import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
|
|
9
9
|
import { r as parseAcceptSignature } from "../accept-Dd__NiUL.mjs";
|
|
10
|
-
import { a as generateCryptoKeyPair } from "../key-
|
|
11
|
-
import { s as signRequest } from "../http-
|
|
12
|
-
import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
|
|
10
|
+
import { a as generateCryptoKeyPair } from "../key-D0ahOVjf.mjs";
|
|
11
|
+
import { s as signRequest } from "../http-lCmKCZBI.mjs";
|
|
12
|
+
import { a as rsaPrivateKey3, c as rsaPublicKey3, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
|
|
13
13
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
14
|
-
import { a as compactJsonLd, p as signJsonLd } from "../ld-
|
|
15
|
-
import { r as signObject } from "../proof-
|
|
16
|
-
import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-
|
|
14
|
+
import { a as compactJsonLd, p as signJsonLd } from "../ld-CJOXIehz.mjs";
|
|
15
|
+
import { r as signObject } from "../proof-DS1Zl6BX.mjs";
|
|
16
|
+
import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-CHoUjmJm.mjs";
|
|
17
17
|
import { t as ActivityListenerSet } from "../activity-listener-ell7W1s9.mjs";
|
|
18
18
|
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
19
19
|
import { Activity, Create, CryptographicKey, Multikey, Note, Person, Tombstone } from "@fedify/vocab";
|
|
@@ -2901,6 +2901,259 @@ test("handleInbox() - authentication bypass vulnerability", async () => {
|
|
|
2901
2901
|
assertEquals(await response.text(), "The signer and the actor do not match.");
|
|
2902
2902
|
assertEquals(processedActivity, void 0, `SECURITY VULNERABILITY: Malicious activity with mismatched signature was processed! Activity ID: ${processedActivity?.id?.href}, Claimed actor: ${processedActivity?.actorId?.href}`);
|
|
2903
2903
|
});
|
|
2904
|
+
test("handleInbox() rejects cross-origin activity IDs", async () => {
|
|
2905
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2906
|
+
const kv = new MemoryKvStore();
|
|
2907
|
+
const inboxListeners = new ActivityListenerSet();
|
|
2908
|
+
let idempotencyCalls = 0;
|
|
2909
|
+
let listenerCalls = 0;
|
|
2910
|
+
inboxListeners.add(Create, () => {
|
|
2911
|
+
listenerCalls++;
|
|
2912
|
+
});
|
|
2913
|
+
const activity = new Create({
|
|
2914
|
+
id: new URL("https://victim.example/activities/1"),
|
|
2915
|
+
actor: rsaPublicKey3.ownerId,
|
|
2916
|
+
object: new Note({ content: "Forged" })
|
|
2917
|
+
});
|
|
2918
|
+
const activityJson = await activity.toJsonLd({
|
|
2919
|
+
format: "compact",
|
|
2920
|
+
contextLoader: mockDocumentLoader
|
|
2921
|
+
});
|
|
2922
|
+
const proofSigned = await signObject(activity, ed25519PrivateKey, ed25519Multikey.id, {
|
|
2923
|
+
contextLoader: mockDocumentLoader,
|
|
2924
|
+
documentLoader: mockDocumentLoader
|
|
2925
|
+
});
|
|
2926
|
+
const requests = [
|
|
2927
|
+
await signRequest(new Request("https://local.example/inbox", {
|
|
2928
|
+
method: "POST",
|
|
2929
|
+
body: JSON.stringify(activityJson)
|
|
2930
|
+
}), rsaPrivateKey3, rsaPublicKey3.id),
|
|
2931
|
+
new Request("https://local.example/inbox", {
|
|
2932
|
+
method: "POST",
|
|
2933
|
+
body: JSON.stringify(await signJsonLd(activityJson, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }))
|
|
2934
|
+
}),
|
|
2935
|
+
new Request("https://local.example/inbox", {
|
|
2936
|
+
method: "POST",
|
|
2937
|
+
body: JSON.stringify(await proofSigned.toJsonLd({
|
|
2938
|
+
format: "compact",
|
|
2939
|
+
contextLoader: mockDocumentLoader
|
|
2940
|
+
}))
|
|
2941
|
+
}),
|
|
2942
|
+
await signRequest(new Request("https://local.example/inbox", {
|
|
2943
|
+
method: "POST",
|
|
2944
|
+
body: JSON.stringify(await new Create({
|
|
2945
|
+
id: new URL("https://example.com/activities/multiple-actors"),
|
|
2946
|
+
actors: [rsaPublicKey3.ownerId, new URL("https://attacker.example/actor")]
|
|
2947
|
+
}).toJsonLd())
|
|
2948
|
+
}), rsaPrivateKey3, rsaPublicKey3.id)
|
|
2949
|
+
];
|
|
2950
|
+
for (const request of requests) {
|
|
2951
|
+
const context = createRequestContext({
|
|
2952
|
+
federation,
|
|
2953
|
+
request,
|
|
2954
|
+
url: new URL(request.url),
|
|
2955
|
+
data: void 0,
|
|
2956
|
+
documentLoader: mockDocumentLoader,
|
|
2957
|
+
contextLoader: mockDocumentLoader
|
|
2958
|
+
});
|
|
2959
|
+
const response = await handleInbox(request, {
|
|
2960
|
+
recipient: null,
|
|
2961
|
+
context,
|
|
2962
|
+
inboxContextFactory(_activity) {
|
|
2963
|
+
return createInboxContext({
|
|
2964
|
+
...context,
|
|
2965
|
+
clone: void 0
|
|
2966
|
+
});
|
|
2967
|
+
},
|
|
2968
|
+
kv,
|
|
2969
|
+
kvPrefixes: {
|
|
2970
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2971
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2972
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
2973
|
+
},
|
|
2974
|
+
actorDispatcher: () => null,
|
|
2975
|
+
inboxListeners,
|
|
2976
|
+
idempotencyStrategy() {
|
|
2977
|
+
idempotencyCalls++;
|
|
2978
|
+
return "forged";
|
|
2979
|
+
},
|
|
2980
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2981
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2982
|
+
skipSignatureVerification: false
|
|
2983
|
+
});
|
|
2984
|
+
assertEquals([response.status, await response.text()], [401, "The activity and its actors do not have the same origin."]);
|
|
2985
|
+
}
|
|
2986
|
+
assertEquals(idempotencyCalls, 0);
|
|
2987
|
+
assertEquals(listenerCalls, 0);
|
|
2988
|
+
});
|
|
2989
|
+
test("handleInbox() preserves the signature-verification opt-out", async () => {
|
|
2990
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2991
|
+
const activity = new Create({
|
|
2992
|
+
id: new URL("https://victim.example/activities/1"),
|
|
2993
|
+
actor: new URL("https://attacker.example/actor")
|
|
2994
|
+
});
|
|
2995
|
+
const request = new Request("https://local.example/inbox", {
|
|
2996
|
+
method: "POST",
|
|
2997
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
2998
|
+
});
|
|
2999
|
+
const context = createRequestContext({
|
|
3000
|
+
federation,
|
|
3001
|
+
request,
|
|
3002
|
+
url: new URL(request.url),
|
|
3003
|
+
data: void 0,
|
|
3004
|
+
documentLoader: mockDocumentLoader
|
|
3005
|
+
});
|
|
3006
|
+
let listenerCalls = 0;
|
|
3007
|
+
const inboxListeners = new ActivityListenerSet();
|
|
3008
|
+
inboxListeners.add(Create, () => {
|
|
3009
|
+
listenerCalls++;
|
|
3010
|
+
});
|
|
3011
|
+
assertEquals((await handleInbox(request, {
|
|
3012
|
+
recipient: null,
|
|
3013
|
+
context,
|
|
3014
|
+
inboxContextFactory(_activity) {
|
|
3015
|
+
return createInboxContext({
|
|
3016
|
+
...context,
|
|
3017
|
+
clone: void 0
|
|
3018
|
+
});
|
|
3019
|
+
},
|
|
3020
|
+
kv: new MemoryKvStore(),
|
|
3021
|
+
kvPrefixes: {
|
|
3022
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
3023
|
+
publicKey: ["_fedify", "publicKey"],
|
|
3024
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
3025
|
+
},
|
|
3026
|
+
actorDispatcher: () => null,
|
|
3027
|
+
inboxListeners,
|
|
3028
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
3029
|
+
signatureTimeWindow: { minutes: 5 },
|
|
3030
|
+
skipSignatureVerification: true
|
|
3031
|
+
})).status, 202);
|
|
3032
|
+
assertEquals(listenerCalls, 1);
|
|
3033
|
+
});
|
|
3034
|
+
test("handleInbox() rejects missing actors before idempotency", async () => {
|
|
3035
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
3036
|
+
const unsignedJson = {
|
|
3037
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
3038
|
+
id: "https://remote.example/activities/actorless",
|
|
3039
|
+
type: "Create"
|
|
3040
|
+
};
|
|
3041
|
+
const requests = [new Request("https://local.example/inbox", {
|
|
3042
|
+
method: "POST",
|
|
3043
|
+
body: JSON.stringify(unsignedJson)
|
|
3044
|
+
}), new Request("https://local.example/inbox", {
|
|
3045
|
+
method: "POST",
|
|
3046
|
+
body: JSON.stringify(await signJsonLd(unsignedJson, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }))
|
|
3047
|
+
})];
|
|
3048
|
+
let idempotencyCalls = 0;
|
|
3049
|
+
for (const request of requests) {
|
|
3050
|
+
const context = createRequestContext({
|
|
3051
|
+
federation,
|
|
3052
|
+
request,
|
|
3053
|
+
url: new URL(request.url),
|
|
3054
|
+
data: void 0,
|
|
3055
|
+
documentLoader: mockDocumentLoader,
|
|
3056
|
+
contextLoader: mockDocumentLoader
|
|
3057
|
+
});
|
|
3058
|
+
const response = await handleInbox(request, {
|
|
3059
|
+
recipient: null,
|
|
3060
|
+
context,
|
|
3061
|
+
inboxContextFactory(_activity) {
|
|
3062
|
+
return createInboxContext({
|
|
3063
|
+
...context,
|
|
3064
|
+
clone: void 0
|
|
3065
|
+
});
|
|
3066
|
+
},
|
|
3067
|
+
kv: new MemoryKvStore(),
|
|
3068
|
+
kvPrefixes: {
|
|
3069
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
3070
|
+
publicKey: ["_fedify", "publicKey"],
|
|
3071
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
3072
|
+
},
|
|
3073
|
+
actorDispatcher: () => null,
|
|
3074
|
+
idempotencyStrategy() {
|
|
3075
|
+
idempotencyCalls++;
|
|
3076
|
+
return "actorless";
|
|
3077
|
+
},
|
|
3078
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
3079
|
+
signatureTimeWindow: { minutes: 5 },
|
|
3080
|
+
skipSignatureVerification: false
|
|
3081
|
+
});
|
|
3082
|
+
assertEquals([response.status, await response.text()], [400, "Missing actor."]);
|
|
3083
|
+
}
|
|
3084
|
+
assertEquals(idempotencyCalls, 0);
|
|
3085
|
+
});
|
|
3086
|
+
test("handleInbox() leaves a rejected activity ID available", async () => {
|
|
3087
|
+
const activityId = new URL("https://example.com/activities/genuine");
|
|
3088
|
+
const attackerId = new URL("https://attacker.example/actor");
|
|
3089
|
+
const attackerKeyId = new URL(`${attackerId.href}#main-key`);
|
|
3090
|
+
const attackerKeys = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
|
|
3091
|
+
const attackerDocument = await new Person({
|
|
3092
|
+
id: attackerId,
|
|
3093
|
+
publicKey: new CryptographicKey({
|
|
3094
|
+
id: attackerKeyId,
|
|
3095
|
+
owner: attackerId,
|
|
3096
|
+
publicKey: attackerKeys.publicKey
|
|
3097
|
+
})
|
|
3098
|
+
}).toJsonLd({ contextLoader: mockDocumentLoader });
|
|
3099
|
+
const documentLoader = (resource) => resource === attackerId.href || resource === attackerKeyId.href ? Promise.resolve({
|
|
3100
|
+
contextUrl: null,
|
|
3101
|
+
documentUrl: resource,
|
|
3102
|
+
document: attackerDocument
|
|
3103
|
+
}) : mockDocumentLoader(resource);
|
|
3104
|
+
for (const strategy of ["per-inbox", "global"]) {
|
|
3105
|
+
const kv = new MemoryKvStore();
|
|
3106
|
+
const federation = createFederation({ kv });
|
|
3107
|
+
const inboxListeners = new ActivityListenerSet();
|
|
3108
|
+
const received = [];
|
|
3109
|
+
inboxListeners.add(Create, (_ctx, activity) => {
|
|
3110
|
+
received.push(activity.actorId.href);
|
|
3111
|
+
});
|
|
3112
|
+
const handle = async (request) => {
|
|
3113
|
+
const context = createRequestContext({
|
|
3114
|
+
federation,
|
|
3115
|
+
request,
|
|
3116
|
+
url: new URL(request.url),
|
|
3117
|
+
data: void 0,
|
|
3118
|
+
documentLoader,
|
|
3119
|
+
contextLoader: mockDocumentLoader
|
|
3120
|
+
});
|
|
3121
|
+
return await handleInbox(request, {
|
|
3122
|
+
recipient: null,
|
|
3123
|
+
context,
|
|
3124
|
+
inboxContextFactory(_activity) {
|
|
3125
|
+
return createInboxContext({
|
|
3126
|
+
...context,
|
|
3127
|
+
clone: void 0
|
|
3128
|
+
});
|
|
3129
|
+
},
|
|
3130
|
+
kv,
|
|
3131
|
+
kvPrefixes: {
|
|
3132
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
3133
|
+
publicKey: ["_fedify", "publicKey"],
|
|
3134
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
3135
|
+
},
|
|
3136
|
+
actorDispatcher: () => null,
|
|
3137
|
+
inboxListeners,
|
|
3138
|
+
idempotencyStrategy: strategy,
|
|
3139
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
3140
|
+
signatureTimeWindow: { minutes: 5 },
|
|
3141
|
+
skipSignatureVerification: false
|
|
3142
|
+
});
|
|
3143
|
+
};
|
|
3144
|
+
const createRequest = async (actor, privateKey, keyId) => await signRequest(new Request("https://local.example/inbox", {
|
|
3145
|
+
method: "POST",
|
|
3146
|
+
body: JSON.stringify(await new Create({
|
|
3147
|
+
id: activityId,
|
|
3148
|
+
actor
|
|
3149
|
+
}).toJsonLd())
|
|
3150
|
+
}), privateKey, keyId);
|
|
3151
|
+
const forged = await handle(await createRequest(attackerId, attackerKeys.privateKey, attackerKeyId));
|
|
3152
|
+
const genuine = await handle(await createRequest(rsaPublicKey3.ownerId, rsaPrivateKey3, rsaPublicKey3.id));
|
|
3153
|
+
assertEquals([forged.status, genuine.status], [401, 202]);
|
|
3154
|
+
assertEquals(received, [rsaPublicKey3.ownerId.href]);
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
2904
3157
|
test("respondWithObjectIfAcceptable", async () => {
|
|
2905
3158
|
let request = new Request("https://example.com/", { headers: { Accept: "application/activity+json" } });
|
|
2906
3159
|
let response = await respondWithObjectIfAcceptable(new Note({
|
|
@@ -2,11 +2,11 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
|
-
import "../std__assert-
|
|
5
|
+
import "../std__assert-DNZ4Liil.mjs";
|
|
6
6
|
import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
|
|
7
7
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
8
|
-
import { r as signObject } from "../proof-
|
|
9
|
-
import { o as createFederation } from "../middleware-
|
|
8
|
+
import { r as signObject } from "../proof-DS1Zl6BX.mjs";
|
|
9
|
+
import { o as createFederation } from "../middleware-CHoUjmJm.mjs";
|
|
10
10
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
11
11
|
import { Create, Follow, Person } from "@fedify/vocab";
|
|
12
12
|
//#region src/federation/idempotency.test.ts
|
|
@@ -2,11 +2,35 @@ import "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
|
-
import { t as assertThrows } from "../assert_throws-
|
|
5
|
+
import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
|
|
6
6
|
import { t as ActivityListenerSet } from "../activity-listener-ell7W1s9.mjs";
|
|
7
|
+
import { t as actorsHaveSameOrigin } from "../inbox-DMATfdEI.mjs";
|
|
7
8
|
import { test } from "@fedify/fixture";
|
|
8
9
|
import { Activity, Create, Invite, Offer, Update } from "@fedify/vocab";
|
|
9
10
|
//#region src/federation/inbox.test.ts
|
|
11
|
+
test("actorsHaveSameOrigin()", () => {
|
|
12
|
+
assertEquals(actorsHaveSameOrigin(new Create({ actor: new URL("https://remote.example/actor") })), true);
|
|
13
|
+
assertEquals(actorsHaveSameOrigin(new Create({
|
|
14
|
+
id: new URL("https://remote.example/activity"),
|
|
15
|
+
actors: [new URL("https://remote.example/actor/1"), new URL("https://remote.example:443/actor/2")]
|
|
16
|
+
})), true);
|
|
17
|
+
for (const actor of [
|
|
18
|
+
"http://remote.example/actor",
|
|
19
|
+
"https://sub.remote.example/actor",
|
|
20
|
+
"https://remote.example:8443/actor"
|
|
21
|
+
]) assertFalse(actorsHaveSameOrigin(new Create({
|
|
22
|
+
id: new URL("https://remote.example/activity"),
|
|
23
|
+
actor: new URL(actor)
|
|
24
|
+
})));
|
|
25
|
+
assertFalse(actorsHaveSameOrigin(new Create({
|
|
26
|
+
id: new URL("urn:example:activity"),
|
|
27
|
+
actor: new URL("urn:example:actor")
|
|
28
|
+
})));
|
|
29
|
+
assertFalse(actorsHaveSameOrigin(new Create({
|
|
30
|
+
id: new URL("https://remote.example/activity"),
|
|
31
|
+
actors: [new URL("https://remote.example/actor/1"), new URL("https://other.example/actor/2")]
|
|
32
|
+
})));
|
|
33
|
+
});
|
|
10
34
|
test("ActivityListenerSet", () => {
|
|
11
35
|
const listeners = new ActivityListenerSet();
|
|
12
36
|
const activity = new Activity({});
|
|
@@ -2,7 +2,7 @@ import { Temporal } from "@js-temporal/polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
|
-
import "../std__assert-
|
|
5
|
+
import "../std__assert-DNZ4Liil.mjs";
|
|
6
6
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
7
7
|
import { test } from "@fedify/fixture";
|
|
8
8
|
//#region src/federation/kv.test.ts
|
|
@@ -4,20 +4,20 @@ globalThis.addEventListener = () => {};
|
|
|
4
4
|
import { t as esm_default } from "../esm-DVILvP5e.mjs";
|
|
5
5
|
import { n as RouterError } from "../router-CrMLXoOr.mjs";
|
|
6
6
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
7
|
-
import "../std__assert-
|
|
8
|
-
import {
|
|
7
|
+
import "../std__assert-DNZ4Liil.mjs";
|
|
8
|
+
import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
|
|
9
9
|
import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
|
|
10
|
-
import { t as
|
|
11
|
-
import { t as
|
|
10
|
+
import { t as assertNotEquals } from "../assert_not_equals-BkxW5QJI.mjs";
|
|
11
|
+
import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
|
|
12
12
|
import { t as assertStrictEquals } from "../assert_strict_equals-Dmjbg-bA.mjs";
|
|
13
|
-
import { l as verifyRequest, s as signRequest } from "../http-
|
|
13
|
+
import { l as verifyRequest, s as signRequest } from "../http-lCmKCZBI.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-BAK-tUlf.mjs";
|
|
15
|
-
import { t as getAuthenticatedDocumentLoader } from "../docloader-
|
|
15
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-BGjq1F5U.mjs";
|
|
16
16
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
17
|
-
import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-
|
|
18
|
-
import { t as doesActorOwnKey } from "../owner-
|
|
19
|
-
import { i as verifyObject, r as signObject } from "../proof-
|
|
20
|
-
import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-
|
|
17
|
+
import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-CJOXIehz.mjs";
|
|
18
|
+
import { t as doesActorOwnKey } from "../owner-BUTMEkbn.mjs";
|
|
19
|
+
import { i as verifyObject, r as signObject } from "../proof-DS1Zl6BX.mjs";
|
|
20
|
+
import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-CHoUjmJm.mjs";
|
|
21
21
|
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
22
22
|
import * as vocab from "@fedify/vocab";
|
|
23
23
|
import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
|
|
@@ -4547,6 +4547,124 @@ test("ContextImpl.routeActivity() marks queued signed activities as non-LDS", as
|
|
|
4547
4547
|
assertEquals(inboxMessage.ldSignatureVerified, false);
|
|
4548
4548
|
assertEquals(inboxMessage.normalizedActivity, void 0);
|
|
4549
4549
|
});
|
|
4550
|
+
for (const queued of [false, true]) test(`ContextImpl.routeActivity() rejects a proof-signed cross-origin ID (queued: ${queued})`, async () => {
|
|
4551
|
+
const messages = [];
|
|
4552
|
+
const queue = {
|
|
4553
|
+
enqueue(message) {
|
|
4554
|
+
messages.push(message);
|
|
4555
|
+
return Promise.resolve();
|
|
4556
|
+
},
|
|
4557
|
+
async listen() {}
|
|
4558
|
+
};
|
|
4559
|
+
const federation = createFederation({
|
|
4560
|
+
kv: new MemoryKvStore(),
|
|
4561
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
4562
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
4563
|
+
queue: queued ? {
|
|
4564
|
+
inbox: queue,
|
|
4565
|
+
outbox: queue
|
|
4566
|
+
} : void 0,
|
|
4567
|
+
manuallyStartQueue: true
|
|
4568
|
+
});
|
|
4569
|
+
let listenerCalls = 0;
|
|
4570
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
4571
|
+
listenerCalls++;
|
|
4572
|
+
});
|
|
4573
|
+
const signed = await signObject(new Create({
|
|
4574
|
+
id: new URL("https://victim.example/activities/1"),
|
|
4575
|
+
actor: ed25519Multikey.controllerId
|
|
4576
|
+
}), ed25519PrivateKey, ed25519Multikey.id, {
|
|
4577
|
+
contextLoader: mockDocumentLoader,
|
|
4578
|
+
documentLoader: mockDocumentLoader
|
|
4579
|
+
});
|
|
4580
|
+
assertFalse(await federation.createContext(new URL("https://local.example/")).routeActivity(null, signed));
|
|
4581
|
+
assertEquals(listenerCalls, 0);
|
|
4582
|
+
assertEquals(messages, []);
|
|
4583
|
+
});
|
|
4584
|
+
test("ContextImpl.routeActivity() rejects a missing actor before routing", async () => {
|
|
4585
|
+
const federation = createFederation({
|
|
4586
|
+
kv: new MemoryKvStore(),
|
|
4587
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
4588
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
4589
|
+
});
|
|
4590
|
+
let listenerCalls = 0;
|
|
4591
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
4592
|
+
listenerCalls++;
|
|
4593
|
+
});
|
|
4594
|
+
assertFalse(await federation.createContext(new URL("https://local.example/")).routeActivity(null, new Create({ id: new URL("https://remote.example/activities/1") })));
|
|
4595
|
+
assertEquals(listenerCalls, 0);
|
|
4596
|
+
});
|
|
4597
|
+
test("FederationImpl.processQueuedTask() rejects untrusted activity origins before KV access", async () => {
|
|
4598
|
+
class TrackingKvStore extends MemoryKvStore {
|
|
4599
|
+
reads = 0;
|
|
4600
|
+
writes = 0;
|
|
4601
|
+
get(key) {
|
|
4602
|
+
this.reads++;
|
|
4603
|
+
return super.get(key);
|
|
4604
|
+
}
|
|
4605
|
+
set(key, value, options) {
|
|
4606
|
+
this.writes++;
|
|
4607
|
+
return super.set(key, value, options);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
for (const activity of [{
|
|
4611
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4612
|
+
id: "https://victim.example/activities/1",
|
|
4613
|
+
type: "Create",
|
|
4614
|
+
actor: "https://attacker.example/actor"
|
|
4615
|
+
}, {
|
|
4616
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4617
|
+
id: "https://victim.example/activities/2",
|
|
4618
|
+
type: "Create"
|
|
4619
|
+
}]) {
|
|
4620
|
+
const kv = new TrackingKvStore();
|
|
4621
|
+
const federation = new FederationImpl({ kv });
|
|
4622
|
+
let listenerCalls = 0;
|
|
4623
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
4624
|
+
listenerCalls++;
|
|
4625
|
+
});
|
|
4626
|
+
await federation.processQueuedTask(void 0, {
|
|
4627
|
+
type: "inbox",
|
|
4628
|
+
id: crypto.randomUUID(),
|
|
4629
|
+
baseUrl: "https://local.example",
|
|
4630
|
+
activity,
|
|
4631
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4632
|
+
attempt: 0,
|
|
4633
|
+
identifier: null,
|
|
4634
|
+
traceContext: {}
|
|
4635
|
+
});
|
|
4636
|
+
assertEquals(listenerCalls, 0);
|
|
4637
|
+
assertEquals(kv.reads, 0);
|
|
4638
|
+
assertEquals(kv.writes, 0);
|
|
4639
|
+
}
|
|
4640
|
+
const kv = new TrackingKvStore();
|
|
4641
|
+
const federation = new FederationImpl({
|
|
4642
|
+
kv,
|
|
4643
|
+
skipSignatureVerification: true
|
|
4644
|
+
});
|
|
4645
|
+
let listenerCalls = 0;
|
|
4646
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
4647
|
+
listenerCalls++;
|
|
4648
|
+
});
|
|
4649
|
+
await federation.processQueuedTask(void 0, {
|
|
4650
|
+
type: "inbox",
|
|
4651
|
+
id: crypto.randomUUID(),
|
|
4652
|
+
baseUrl: "https://local.example",
|
|
4653
|
+
activity: {
|
|
4654
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4655
|
+
id: "https://victim.example/activities/opt-out",
|
|
4656
|
+
type: "Create",
|
|
4657
|
+
actor: "https://attacker.example/actor"
|
|
4658
|
+
},
|
|
4659
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4660
|
+
attempt: 0,
|
|
4661
|
+
identifier: null,
|
|
4662
|
+
traceContext: {}
|
|
4663
|
+
});
|
|
4664
|
+
assertEquals(listenerCalls, 1);
|
|
4665
|
+
assertEquals(kv.reads, 1);
|
|
4666
|
+
assertEquals(kv.writes, 1);
|
|
4667
|
+
});
|
|
4550
4668
|
test("ContextImpl.getCollectionUri()", () => {
|
|
4551
4669
|
const federation = new FederationImpl({ kv: new MemoryKvStore() });
|
|
4552
4670
|
const base = "https://example.com";
|