@fedify/fedify 2.1.24 → 2.1.26
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-BatYe9a2.mjs → builder-9KbL1CoC.mjs} +11 -3
- package/dist/{client-DNiBssfx.mjs → client-DM72NZuk.mjs} +7 -3
- 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-D4s0Wk3N.d.cts → context-BNwv9Q1v.d.cts} +1 -1
- package/dist/{context-BgU5UWTx.d.ts → context-DLLaDau8.d.ts} +1 -1
- package/dist/{deno-BXkm_rws.mjs → deno-ChmEXNOv.mjs} +1 -1
- package/dist/docloader-DEVImb5e.mjs +78 -0
- package/dist/federation/builder.test.mjs +37 -2
- package/dist/federation/collection.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +7 -7
- package/dist/federation/idempotency.test.mjs +3 -3
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/kv.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +249 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +98 -43
- package/dist/federation/negotiation.test.mjs +1 -1
- package/dist/federation/router.test.mjs +1 -1
- package/dist/federation/send.test.mjs +68 -5
- package/dist/federation/temporal.test.mjs +2 -2
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{http-7FmLhLDv.cjs → http-BOH-uFLj.cjs} +6 -2
- package/dist/{http-UyebnM2c.mjs → http-B_M80hBW.mjs} +7 -3
- package/dist/{http-Chr54tTL.js → http-CeK9QmL_.js} +6 -2
- package/dist/{inbox-ByQ2iD14.mjs → inbox-B_O6TjZS.mjs} +1 -1
- package/dist/{key-A8SuFznD.mjs → key-BJcK-1Av.mjs} +1 -1
- package/dist/{kv-gJ8LYbxX.d.cts → kv-CZKl1IUS.d.cts} +3 -2
- package/dist/{kv-D6hNiMTK.d.ts → kv-CxaI4eGo.d.ts} +3 -2
- package/dist/{kv-cache-DJhBDA9T.cjs → kv-cache-BlOG31SS.cjs} +34 -8
- package/dist/{kv-cache-J6hmiAEd.js → kv-cache-CEJICY0R.js} +35 -9
- package/dist/{ld-BSvHU-S0.mjs → ld-YAXn6sG8.mjs} +3 -3
- package/dist/{middleware-Do50b12a.js → middleware-C-45wr85.js} +30 -10
- package/dist/{middleware-Dy18K1xP.mjs → middleware-Co_QOX9H.mjs} +21 -18
- package/dist/{middleware-CCYqHlzn.cjs → middleware-DHGVHIUj.cjs} +31 -11
- package/dist/{middleware-DvxNTKpl.mjs → middleware-DmTWC5VG.mjs} +1 -1
- package/dist/{middleware-Bddj287b.cjs → middleware-dGW2gSDG.cjs} +1 -1
- package/dist/{mod-Bx80LUTA.d.cts → mod-BmlfRBmU.d.cts} +1 -1
- package/dist/{mod-DcAiY74j.d.ts → mod-CWJNbyju.d.ts} +1 -1
- package/dist/{mod-DniKVJ_C.d.cts → mod-Cn9GJ5ex.d.cts} +3 -1
- package/dist/{mod-CMK_S-eX.d.ts → mod-DNEgTI8M.d.ts} +3 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +4 -4
- package/dist/mod.d.ts +4 -4
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +50 -2
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +1 -1
- package/dist/otel/exporter.test.mjs +1 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{owner-CaaCVHsG.mjs → owner-B3ZkY-Ov.mjs} +2 -2
- package/dist/{proof-BrOQrPQf.js → proof-BeGIkK97.js} +2 -2
- package/dist/{proof-_AyA4r7z.mjs → proof-DIGrz49z.mjs} +2 -2
- package/dist/{proof-CaCUMcvk.cjs → proof-DzOl2GlQ.cjs} +2 -2
- package/dist/{send-B1CRgb14.mjs → send-N2w1IwXT.mjs} +12 -3
- package/dist/sig/http.test.mjs +43 -3
- package/dist/sig/key.test.mjs +2 -2
- package/dist/sig/ld.test.mjs +5 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +2 -2
- package/dist/sig/proof.test.mjs +2 -2
- package/dist/{std__assert-Duiq_YC9.mjs → std__assert-BUOJXi-Z.mjs} +1 -26
- package/dist/{temporal-Pmk5ouB1.mjs → temporal-CpT0u5Mx.mjs} +1 -1
- package/dist/{types-DWs-qZaY.cjs → types-DRPjmHVV.cjs} +7 -3
- package/dist/{types-sXITXcWa.js → types-miAYfiGO.js} +7 -3
- package/dist/utils/docloader.test.mjs +299 -3
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +7 -7
- package/dist/docloader-DdEK2tXP.mjs +0 -52
|
@@ -2,8 +2,8 @@ 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-
|
|
6
|
-
import { t as InboxListenerSet } from "./inbox-
|
|
5
|
+
import { n as version, t as name } from "./deno-ChmEXNOv.mjs";
|
|
6
|
+
import { t as InboxListenerSet } from "./inbox-B_O6TjZS.mjs";
|
|
7
7
|
import { getTypeId } from "@fedify/vocab";
|
|
8
8
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
9
9
|
import { getLogger } from "@logtape/logtape";
|
|
@@ -35,6 +35,7 @@ var FederationBuilderImpl = class {
|
|
|
35
35
|
* Symbol registry for unique identification of unnamed symbols.
|
|
36
36
|
*/
|
|
37
37
|
#symbolRegistry = /* @__PURE__ */ new Map();
|
|
38
|
+
#collectionNames = /* @__PURE__ */ new Map();
|
|
38
39
|
constructor() {
|
|
39
40
|
this.router = new Router();
|
|
40
41
|
this.objectCallbacks = {};
|
|
@@ -43,7 +44,7 @@ var FederationBuilderImpl = class {
|
|
|
43
44
|
this.collectionTypeIds = {};
|
|
44
45
|
}
|
|
45
46
|
async build(options) {
|
|
46
|
-
const { FederationImpl } = await import("./middleware-
|
|
47
|
+
const { FederationImpl } = await import("./middleware-DmTWC5VG.mjs");
|
|
47
48
|
const f = new FederationImpl(options);
|
|
48
49
|
const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
|
|
49
50
|
f.router = this.router.clone();
|
|
@@ -56,6 +57,8 @@ var FederationBuilderImpl = class {
|
|
|
56
57
|
f.objectTypeIds = { ...this.objectTypeIds };
|
|
57
58
|
f.collectionCallbacks = { ...this.collectionCallbacks };
|
|
58
59
|
f.collectionTypeIds = { ...this.collectionTypeIds };
|
|
60
|
+
f.#symbolRegistry = new Map(this.#symbolRegistry);
|
|
61
|
+
f.#collectionNames = new Map(this.#collectionNames);
|
|
59
62
|
f.inboxPath = this.inboxPath;
|
|
60
63
|
f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks };
|
|
61
64
|
f.outboxCallbacks = this.outboxCallbacks == null ? void 0 : { ...this.outboxCallbacks };
|
|
@@ -468,6 +471,7 @@ var FederationBuilderImpl = class {
|
|
|
468
471
|
if (this.router.has(routeName)) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
|
469
472
|
if (this.collectionCallbacks[name] != null) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
|
470
473
|
if (this.router.add(path, routeName).size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable.");
|
|
474
|
+
this.#collectionNames.set(routeName, name);
|
|
471
475
|
const callbacks = { dispatcher };
|
|
472
476
|
this.collectionCallbacks[name] = callbacks;
|
|
473
477
|
this.collectionTypeIds[name] = itemType;
|
|
@@ -504,6 +508,10 @@ var FederationBuilderImpl = class {
|
|
|
504
508
|
const routeName = this.#uniqueCollectionId(name);
|
|
505
509
|
return this.router.build(`collection:${routeName}`, values) ?? this.router.build(`orderedCollection:${routeName}`, values);
|
|
506
510
|
}
|
|
511
|
+
/** Resolves a custom collection route to its original name. */
|
|
512
|
+
getCollectionName(routeName) {
|
|
513
|
+
return this.#collectionNames.get(routeName) ?? routeName.replace(/^(collection|orderedCollection):/, "");
|
|
514
|
+
}
|
|
507
515
|
setOutboxPermanentFailureHandler(handler) {
|
|
508
516
|
this.outboxPermanentFailureHandler = handler;
|
|
509
517
|
}
|
|
@@ -93,8 +93,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
93
93
|
* bounce the request to an internal address.
|
|
94
94
|
* @param url The URL to fetch.
|
|
95
95
|
* @param headers The request headers.
|
|
96
|
-
* @returns The final response, or `null` if the URL is disallowed
|
|
97
|
-
* are too many redirects.
|
|
96
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
97
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
98
98
|
*/
|
|
99
99
|
async function fetchPublicUrl(url, headers) {
|
|
100
100
|
let current = url instanceof URL ? url : new URL(url);
|
|
@@ -104,7 +104,11 @@ async function fetchPublicUrl(url, headers) {
|
|
|
104
104
|
await validatePublicUrl(current.href);
|
|
105
105
|
} catch (e) {
|
|
106
106
|
if (e instanceof UrlError) {
|
|
107
|
-
logger.error("
|
|
107
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
108
|
+
url: current.href,
|
|
109
|
+
error: e
|
|
110
|
+
});
|
|
111
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
108
112
|
url: current.href,
|
|
109
113
|
error: e
|
|
110
114
|
});
|
package/dist/compat/mod.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { Ct as ActivityTransformer, n as Context } from "../context-
|
|
2
|
+
import { Ct as ActivityTransformer, n as Context } from "../context-BNwv9Q1v.cjs";
|
|
3
3
|
import { Activity } from "@fedify/vocab";
|
|
4
4
|
|
|
5
5
|
//#region src/compat/transformers.d.ts
|
package/dist/compat/mod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { Ct as ActivityTransformer, n as Context } from "../context-
|
|
2
|
+
import { Ct as ActivityTransformer, n as Context } from "../context-DLLaDau8.js";
|
|
3
3
|
import { Activity } from "@fedify/vocab";
|
|
4
4
|
|
|
5
5
|
//#region src/compat/transformers.d.ts
|
|
@@ -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-BrZHNugx.mjs";
|
|
7
|
-
import { _ as autoIdAssigner, g as actorDehydrator, n as FederationImpl } from "../middleware-
|
|
7
|
+
import { _ as autoIdAssigner, g as actorDehydrator, n as FederationImpl } from "../middleware-Co_QOX9H.mjs";
|
|
8
8
|
import { test } from "@fedify/fixture";
|
|
9
9
|
import { Follow, Person } from "@fedify/vocab";
|
|
10
10
|
//#region src/compat/transformers.test.ts
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-CAM_bQXx.cjs";
|
|
3
3
|
import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-DqZhyEso.cjs";
|
|
4
4
|
import { n as GetKeyOwnerOptions } from "./owner-B3hNQCG8.cjs";
|
|
5
|
-
import { n as KvStore, t as KvKey } from "./kv-
|
|
5
|
+
import { n as KvStore, t as KvKey } from "./kv-CZKl1IUS.cjs";
|
|
6
6
|
import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, TraverseCollectionOptions } from "@fedify/vocab";
|
|
7
7
|
import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
8
8
|
import { Link as Link$1, LookupWebFingerOptions, ResourceDescriptor } from "@fedify/webfinger";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-CSddvgWN.js";
|
|
3
3
|
import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-Dl00HXr4.js";
|
|
4
4
|
import { n as GetKeyOwnerOptions } from "./owner-CiBPcC6F.js";
|
|
5
|
-
import { n as KvStore, t as KvKey } from "./kv-
|
|
5
|
+
import { n as KvStore, t as KvKey } from "./kv-CxaI4eGo.js";
|
|
6
6
|
import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, TraverseCollectionOptions } from "@fedify/vocab";
|
|
7
7
|
import { Span, Tracer, TracerProvider } from "@opentelemetry/api";
|
|
8
8
|
import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import "@js-temporal/polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { s as validateCryptoKey } from "./key-BJcK-1Av.mjs";
|
|
5
|
+
import { n as doubleKnock } from "./http-B_M80hBW.mjs";
|
|
6
|
+
import { curry } from "es-toolkit";
|
|
7
|
+
import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
8
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
|
+
//#region src/utils/docloader.ts
|
|
10
|
+
const logger = getLogger([
|
|
11
|
+
"fedify",
|
|
12
|
+
"utils",
|
|
13
|
+
"docloader"
|
|
14
|
+
]);
|
|
15
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
16
|
+
/**
|
|
17
|
+
* Gets an authenticated {@link DocumentLoader} for the given identity.
|
|
18
|
+
* Note that an authenticated document loader intentionally does not cache
|
|
19
|
+
* the fetched documents.
|
|
20
|
+
* At most 20 HTTP redirects and alternate document links are followed in total
|
|
21
|
+
* per call. Revisiting a URL within that chain throws a {@link FetchError}.
|
|
22
|
+
* @param identity The identity to get the document loader for.
|
|
23
|
+
* The actor's key pair.
|
|
24
|
+
* @param options The options for the document loader.
|
|
25
|
+
* @returns The authenticated document loader.
|
|
26
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
27
|
+
* @since 0.4.0
|
|
28
|
+
*/
|
|
29
|
+
function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
|
|
30
|
+
validateCryptoKey(identity.privateKey);
|
|
31
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
32
|
+
options?.signal?.throwIfAborted();
|
|
33
|
+
let currentUrl = new URL(url).href;
|
|
34
|
+
await validateUrl(currentUrl);
|
|
35
|
+
visited.add(currentUrl);
|
|
36
|
+
const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
|
|
37
|
+
function follow(nextUrl) {
|
|
38
|
+
options?.signal?.throwIfAborted();
|
|
39
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
40
|
+
if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
|
|
41
|
+
redirected++;
|
|
42
|
+
}
|
|
43
|
+
const response = await doubleKnock(originalRequest, identity, {
|
|
44
|
+
specDeterminer,
|
|
45
|
+
log: curry(logRequest)(logger),
|
|
46
|
+
tracerProvider,
|
|
47
|
+
signal: options?.signal,
|
|
48
|
+
validateRedirect: async (nextUrl) => {
|
|
49
|
+
follow(nextUrl);
|
|
50
|
+
await validateUrl(nextUrl);
|
|
51
|
+
visited.add(nextUrl);
|
|
52
|
+
currentUrl = nextUrl;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return getRemoteDocument(currentUrl, response, (alternateUrl) => {
|
|
56
|
+
follow(alternateUrl);
|
|
57
|
+
return load(alternateUrl, options, redirected, visited);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function validateUrl(url) {
|
|
61
|
+
if (!allowPrivateAddress) try {
|
|
62
|
+
await validatePublicUrl(url);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
65
|
+
url,
|
|
66
|
+
error
|
|
67
|
+
});
|
|
68
|
+
else logger.error("Disallowed private URL: {url}", {
|
|
69
|
+
url,
|
|
70
|
+
error
|
|
71
|
+
});
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return (url, options) => load(url, options);
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { getAuthenticatedDocumentLoader as t };
|
|
@@ -2,10 +2,10 @@ 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 {
|
|
5
|
+
import { i as assertExists, n as assertStrictEquals } from "../std__assert-BUOJXi-Z.mjs";
|
|
6
6
|
import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
|
|
7
7
|
import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
|
|
8
|
-
import { n as createFederationBuilder } from "../builder-
|
|
8
|
+
import { n as createFederationBuilder } from "../builder-9KbL1CoC.mjs";
|
|
9
9
|
import { test } from "@fedify/fixture";
|
|
10
10
|
import { Activity, Note, Person } from "@fedify/vocab";
|
|
11
11
|
//#region src/federation/builder.test.ts
|
|
@@ -168,6 +168,41 @@ test("FederationBuilder", async (t) => {
|
|
|
168
168
|
assertEquals(impl.collectionTypeIds["test"], Note);
|
|
169
169
|
assertEquals((await impl.fetch(new Request("https://example.com/c/x", { headers: { accept: "application/activity+json" } }), { contextData: void 0 })).status, 200);
|
|
170
170
|
});
|
|
171
|
+
await t.step("preserves symbol-named collections through build()", async () => {
|
|
172
|
+
const builder = createFederationBuilder();
|
|
173
|
+
const collection = Symbol("saved");
|
|
174
|
+
const ordered = Symbol("saved");
|
|
175
|
+
builder.setCollectionDispatcher(collection, Note, "/users/{identifier}/saved", () => ({ items: [] }));
|
|
176
|
+
builder.setOrderedCollectionDispatcher(ordered, Note, "/users/{identifier}/ordered-saved", () => ({ items: [] }));
|
|
177
|
+
for (let build = 0; build < 2; build++) {
|
|
178
|
+
const federation = await builder.build({ kv: new MemoryKvStore() });
|
|
179
|
+
const ctx = federation.createContext(new URL("https://example.com/"));
|
|
180
|
+
for (const [name, path, type] of [[
|
|
181
|
+
collection,
|
|
182
|
+
"/users/alice/saved",
|
|
183
|
+
"collection"
|
|
184
|
+
], [
|
|
185
|
+
ordered,
|
|
186
|
+
"/users/alice/ordered-saved",
|
|
187
|
+
"orderedCollection"
|
|
188
|
+
]]) {
|
|
189
|
+
const uri = ctx.getCollectionUri(name, { identifier: "alice" });
|
|
190
|
+
assertEquals(uri.pathname, path);
|
|
191
|
+
const parsed = ctx.parseUri(uri);
|
|
192
|
+
assertEquals(parsed?.type, type);
|
|
193
|
+
if (parsed?.type !== "collection" && parsed?.type !== "orderedCollection") throw new Error("Expected a custom collection URI");
|
|
194
|
+
assertStrictEquals(parsed.name, name);
|
|
195
|
+
assertStrictEquals(parsed.class, Note);
|
|
196
|
+
assertEquals(parsed.values, { identifier: "alice" });
|
|
197
|
+
for (const cursor of [null, "next"]) {
|
|
198
|
+
const pageUri = new URL(uri);
|
|
199
|
+
if (cursor != null) pageUri.searchParams.set("cursor", cursor);
|
|
200
|
+
assertEquals((await federation.fetch(new Request(pageUri, { headers: { accept: "application/activity+json" } }), { contextData: void 0 })).status, 200);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
assertThrows(() => federation.setCollectionDispatcher(collection, Note, "/users/{identifier}/duplicate", () => ({ items: [] })));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
171
206
|
});
|
|
172
207
|
//#endregion
|
|
173
208
|
export {};
|
|
@@ -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-BUOJXi-Z.mjs";
|
|
6
6
|
import { n as digest, t as buildCollectionSynchronizationHeader } from "../collection-ChgDTHLz.mjs";
|
|
7
7
|
import { test } from "@fedify/fixture";
|
|
8
8
|
import { decodeHex } from "byte-encodings/hex";
|
|
@@ -3,18 +3,18 @@ import "urlpattern-polyfill";
|
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { n as createRequestContext, t as createInboxContext } from "../context-Juj6bdHC.mjs";
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
6
|
-
import "../std__assert-
|
|
6
|
+
import "../std__assert-BUOJXi-Z.mjs";
|
|
7
7
|
import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
|
|
8
8
|
import { t as assert } from "../assert-BhQd8QHP.mjs";
|
|
9
9
|
import { r as parseAcceptSignature } from "../accept-BavzedPB.mjs";
|
|
10
|
-
import { a as generateCryptoKeyPair } from "../key-
|
|
11
|
-
import { s as signRequest } from "../http-
|
|
10
|
+
import { a as generateCryptoKeyPair } from "../key-BJcK-1Av.mjs";
|
|
11
|
+
import { s as signRequest } from "../http-B_M80hBW.mjs";
|
|
12
12
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-CtZLJq76.mjs";
|
|
13
|
-
import { a as compactJsonLd, f as signJsonLd } from "../ld-
|
|
14
|
-
import { n as signObject } from "../proof-
|
|
13
|
+
import { a as compactJsonLd, f as signJsonLd } from "../ld-YAXn6sG8.mjs";
|
|
14
|
+
import { n as signObject } from "../proof-DIGrz49z.mjs";
|
|
15
15
|
import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
|
|
16
|
-
import { a as createFederation, c as handleCollection, d as handleObject, f as respondWithObject, l as handleCustomCollection, p as respondWithObjectIfAcceptable, s as handleActor, u as handleInbox } from "../middleware-
|
|
17
|
-
import { t as InboxListenerSet } from "../inbox-
|
|
16
|
+
import { a as createFederation, c as handleCollection, d as handleObject, f as respondWithObject, l as handleCustomCollection, p as respondWithObjectIfAcceptable, s as handleActor, u as handleInbox } from "../middleware-Co_QOX9H.mjs";
|
|
17
|
+
import { t as InboxListenerSet } from "../inbox-B_O6TjZS.mjs";
|
|
18
18
|
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
19
19
|
import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
|
|
20
20
|
import { FetchError } from "@fedify/vocab-runtime";
|
|
@@ -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-BUOJXi-Z.mjs";
|
|
6
6
|
import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
|
|
7
|
-
import { n as signObject } from "../proof-
|
|
7
|
+
import { n as signObject } from "../proof-DIGrz49z.mjs";
|
|
8
8
|
import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
|
|
9
|
-
import { a as createFederation } from "../middleware-
|
|
9
|
+
import { a as createFederation } from "../middleware-Co_QOX9H.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
|
|
@@ -3,7 +3,7 @@ import "urlpattern-polyfill";
|
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
5
5
|
import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
|
|
6
|
-
import { t as InboxListenerSet } from "../inbox-
|
|
6
|
+
import { t as InboxListenerSet } from "../inbox-B_O6TjZS.mjs";
|
|
7
7
|
import { test } from "@fedify/fixture";
|
|
8
8
|
import { Activity, Create, Invite, Offer, Update } from "@fedify/vocab";
|
|
9
9
|
//#region src/federation/inbox.test.ts
|
|
@@ -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-BUOJXi-Z.mjs";
|
|
6
6
|
import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
|
|
7
7
|
import { test } from "@fedify/fixture";
|
|
8
8
|
//#region src/federation/kv.test.ts
|
|
@@ -4,23 +4,24 @@ 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 { n as assertStrictEquals } from "../std__assert-
|
|
7
|
+
import { n as assertStrictEquals } from "../std__assert-BUOJXi-Z.mjs";
|
|
8
8
|
import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
|
|
9
9
|
import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
|
|
10
10
|
import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
|
|
11
11
|
import { t as assertNotEquals } from "../assert_not_equals--wG9hV7u.mjs";
|
|
12
|
-
import { l as verifyRequest, s as signRequest } from "../http-
|
|
12
|
+
import { l as verifyRequest, s as signRequest } from "../http-B_M80hBW.mjs";
|
|
13
13
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
|
|
14
|
-
import { t as getAuthenticatedDocumentLoader } from "../docloader-
|
|
15
|
-
import { a as compactJsonLd, f as signJsonLd, m as verifyJsonLd, s as detachSignature } from "../ld-
|
|
16
|
-
import { t as doesActorOwnKey } from "../owner-
|
|
17
|
-
import { n as signObject, r as verifyObject } from "../proof-
|
|
14
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-DEVImb5e.mjs";
|
|
15
|
+
import { a as compactJsonLd, f as signJsonLd, m as verifyJsonLd, s as detachSignature } from "../ld-YAXn6sG8.mjs";
|
|
16
|
+
import { t as doesActorOwnKey } from "../owner-B3ZkY-Ov.mjs";
|
|
17
|
+
import { n as signObject, r as verifyObject } from "../proof-DIGrz49z.mjs";
|
|
18
18
|
import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
|
|
19
|
-
import { a as createFederation, i as KvSpecDeterminer, n as FederationImpl, r as InboxContextImpl, t as ContextImpl } from "../middleware-
|
|
19
|
+
import { a as createFederation, i as KvSpecDeterminer, n as FederationImpl, r as InboxContextImpl, t as ContextImpl } from "../middleware-Co_QOX9H.mjs";
|
|
20
20
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
21
21
|
import * as vocab from "@fedify/vocab";
|
|
22
22
|
import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
|
|
23
23
|
import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
24
|
+
import dns from "node:dns/promises";
|
|
24
25
|
//#region ../fixture/src/fixtures/example.com/create.json
|
|
25
26
|
var id$2 = "https://example.com/create";
|
|
26
27
|
var create_default = {
|
|
@@ -717,6 +718,31 @@ test({
|
|
|
717
718
|
esm_default.hardReset();
|
|
718
719
|
}
|
|
719
720
|
});
|
|
721
|
+
test("Federation.fetch() [rfc9421] multiple POST signatures", async (t) => {
|
|
722
|
+
for (const validSecond of [true, false]) await t.step(validSecond ? "valid second signature" : "both signatures invalid", async () => {
|
|
723
|
+
const inbox = [];
|
|
724
|
+
const federation = createFederation({
|
|
725
|
+
kv: new MemoryKvStore(),
|
|
726
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
727
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
728
|
+
});
|
|
729
|
+
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new vocab.Person({ id: ctx.getActorUri(identifier) }));
|
|
730
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, activity) => {
|
|
731
|
+
inbox.push(activity.id.href);
|
|
732
|
+
});
|
|
733
|
+
const signed = await signRequest(new Request("https://example.com/inbox", {
|
|
734
|
+
method: "POST",
|
|
735
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
736
|
+
body: JSON.stringify(create_default)
|
|
737
|
+
}), rsaPrivateKey2, rsaPublicKey2.id, { spec: "rfc9421" });
|
|
738
|
+
const input = signed.headers.get("Signature-Input");
|
|
739
|
+
const signature = signed.headers.get("Signature");
|
|
740
|
+
signed.headers.set("Signature-Input", `${input}, ${input.replace(/^sig1=/, "sig2=")}`);
|
|
741
|
+
signed.headers.set("Signature", `sig1=:AAAAAA==:, ${validSecond ? signature.replace(/^sig1=/, "sig2=") : "sig2=:AAAAAA==:"}`);
|
|
742
|
+
assertEquals((await federation.fetch(signed, { contextData: void 0 })).status, validSecond ? 202 : 401);
|
|
743
|
+
assertEquals(inbox, validSecond ? [id$2] : []);
|
|
744
|
+
});
|
|
745
|
+
});
|
|
720
746
|
test("Federation.fetch()", async (t) => {
|
|
721
747
|
esm_default.spyGlobal();
|
|
722
748
|
esm_default.get("https://example.com/key2", {
|
|
@@ -1426,6 +1452,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
1426
1452
|
const kv = new MemoryKvStore();
|
|
1427
1453
|
const queuedMessages = [];
|
|
1428
1454
|
const federation = new FederationImpl({
|
|
1455
|
+
allowPrivateAddress: true,
|
|
1429
1456
|
kv,
|
|
1430
1457
|
queue: {
|
|
1431
1458
|
nativeRetrial: true,
|
|
@@ -1488,6 +1515,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
1488
1515
|
const kv = new MemoryKvStore();
|
|
1489
1516
|
let queuedMessages = [];
|
|
1490
1517
|
const federation = new FederationImpl({
|
|
1518
|
+
allowPrivateAddress: true,
|
|
1491
1519
|
kv,
|
|
1492
1520
|
queue: {
|
|
1493
1521
|
enqueue(message, _options) {
|
|
@@ -2982,6 +3010,105 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
2982
3010
|
assertEquals(queuedMessages, []);
|
|
2983
3011
|
});
|
|
2984
3012
|
});
|
|
3013
|
+
test({
|
|
3014
|
+
name: "FederationImpl.processQueuedTask() retries DNS failures",
|
|
3015
|
+
ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted",
|
|
3016
|
+
async fn(t) {
|
|
3017
|
+
for (const redirected of [false, true]) await t.step(`redirected: ${redirected}`, async () => {
|
|
3018
|
+
const destination = "https://delivery.invalid/inbox";
|
|
3019
|
+
const publicInbox = "https://8.8.8.8/inbox";
|
|
3020
|
+
const queuedMessages = [];
|
|
3021
|
+
const delays = [];
|
|
3022
|
+
const errors = [];
|
|
3023
|
+
const activities = [];
|
|
3024
|
+
const attempts = [];
|
|
3025
|
+
const delay = Temporal.Duration.from({ seconds: 5 });
|
|
3026
|
+
const federation = new FederationImpl({
|
|
3027
|
+
kv: new MemoryKvStore(),
|
|
3028
|
+
queue: {
|
|
3029
|
+
enqueue(message, options) {
|
|
3030
|
+
queuedMessages.push(message);
|
|
3031
|
+
delays.push(options?.delay);
|
|
3032
|
+
return Promise.resolve();
|
|
3033
|
+
},
|
|
3034
|
+
listen() {
|
|
3035
|
+
return Promise.resolve();
|
|
3036
|
+
}
|
|
3037
|
+
},
|
|
3038
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
3039
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
3040
|
+
onOutboxError(error, activity) {
|
|
3041
|
+
errors.push(error);
|
|
3042
|
+
activities.push(activity);
|
|
3043
|
+
},
|
|
3044
|
+
outboxRetryPolicy(options) {
|
|
3045
|
+
attempts.push(options.attempts);
|
|
3046
|
+
return delay;
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
const message = {
|
|
3050
|
+
type: "outbox",
|
|
3051
|
+
id: crypto.randomUUID(),
|
|
3052
|
+
baseUrl: "https://example.com",
|
|
3053
|
+
keys: [],
|
|
3054
|
+
activity: {
|
|
3055
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
3056
|
+
type: "Create",
|
|
3057
|
+
id: "https://example.com/activities/dns-failure",
|
|
3058
|
+
actor: "https://example.com/users/alice",
|
|
3059
|
+
object: {
|
|
3060
|
+
type: "Note",
|
|
3061
|
+
content: "test"
|
|
3062
|
+
}
|
|
3063
|
+
},
|
|
3064
|
+
activityType: "https://www.w3.org/ns/activitystreams#Create",
|
|
3065
|
+
inbox: redirected ? publicInbox : destination,
|
|
3066
|
+
sharedInbox: false,
|
|
3067
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3068
|
+
attempt: 0,
|
|
3069
|
+
headers: {},
|
|
3070
|
+
traceContext: {}
|
|
3071
|
+
};
|
|
3072
|
+
const originalLookup = dns.lookup;
|
|
3073
|
+
const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
|
|
3074
|
+
const lookups = [];
|
|
3075
|
+
dns.lookup = ((hostname) => {
|
|
3076
|
+
lookups.push(hostname);
|
|
3077
|
+
return Promise.reject(resolverError);
|
|
3078
|
+
});
|
|
3079
|
+
try {
|
|
3080
|
+
esm_default.mockGlobal().catch(202);
|
|
3081
|
+
if (redirected) esm_default.route(publicInbox, {
|
|
3082
|
+
status: 307,
|
|
3083
|
+
headers: { Location: destination }
|
|
3084
|
+
});
|
|
3085
|
+
await federation.processQueuedTask(void 0, message);
|
|
3086
|
+
assertEquals(errors.length, 1);
|
|
3087
|
+
const error = errors[0];
|
|
3088
|
+
assertInstanceOf(error, FetchError);
|
|
3089
|
+
assertEquals(error.url.href, destination);
|
|
3090
|
+
assertInstanceOf(error.cause, UrlError);
|
|
3091
|
+
assertEquals(error.cause.reason, "dns");
|
|
3092
|
+
assertStrictEquals(error.cause.cause, resolverError);
|
|
3093
|
+
assertEquals(activities.length, 1);
|
|
3094
|
+
assertInstanceOf(activities[0], Create);
|
|
3095
|
+
assertEquals(activities[0].id?.href, "https://example.com/activities/dns-failure");
|
|
3096
|
+
assertEquals(attempts, [0]);
|
|
3097
|
+
assertEquals(queuedMessages, [{
|
|
3098
|
+
...message,
|
|
3099
|
+
attempt: 1
|
|
3100
|
+
}]);
|
|
3101
|
+
assertEquals(delays, [delay]);
|
|
3102
|
+
assertEquals(lookups, ["delivery.invalid"]);
|
|
3103
|
+
assertEquals(esm_default.callHistory.calls(destination).length, 0);
|
|
3104
|
+
assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
|
|
3105
|
+
} finally {
|
|
3106
|
+
dns.lookup = originalLookup;
|
|
3107
|
+
esm_default.hardReset();
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
2985
3112
|
test("FederationImpl.processQueuedTask() permanent failure", async (t) => {
|
|
2986
3113
|
esm_default.spyGlobal();
|
|
2987
3114
|
esm_default.post("https://gone.example/inbox", {
|
|
@@ -3670,6 +3797,80 @@ test({
|
|
|
3670
3797
|
]);
|
|
3671
3798
|
}
|
|
3672
3799
|
});
|
|
3800
|
+
for (const queued of [false, true]) test(`ContextImpl.routeActivity() routes the fetched document (queued: ${queued})`, async () => {
|
|
3801
|
+
const id = new URL("https://example.com/verified-create");
|
|
3802
|
+
const genuine = new Create({
|
|
3803
|
+
id,
|
|
3804
|
+
actor: new URL("https://example.com/person"),
|
|
3805
|
+
object: new vocab.Note({ content: "Genuine content" }),
|
|
3806
|
+
to: new URL("https://example.com/recipient")
|
|
3807
|
+
});
|
|
3808
|
+
const genuineJson = await genuine.toJsonLd({ contextLoader: mockDocumentLoader });
|
|
3809
|
+
const forged = new Create({
|
|
3810
|
+
id,
|
|
3811
|
+
actor: new URL("https://victim.example/actor"),
|
|
3812
|
+
object: new vocab.Note({ content: "Forged content" }),
|
|
3813
|
+
to: new URL("https://attacker.example/recipient")
|
|
3814
|
+
});
|
|
3815
|
+
const messages = [];
|
|
3816
|
+
const queue = {
|
|
3817
|
+
enqueue(message) {
|
|
3818
|
+
messages.push(message);
|
|
3819
|
+
return Promise.resolve();
|
|
3820
|
+
},
|
|
3821
|
+
async listen() {}
|
|
3822
|
+
};
|
|
3823
|
+
const federation = createFederation({
|
|
3824
|
+
kv: new MemoryKvStore(),
|
|
3825
|
+
documentLoaderFactory: () => async (url, options) => url === id.href ? {
|
|
3826
|
+
document: genuineJson,
|
|
3827
|
+
documentUrl: id.href,
|
|
3828
|
+
contextUrl: null
|
|
3829
|
+
} : await mockDocumentLoader(url, options),
|
|
3830
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
3831
|
+
queue: {
|
|
3832
|
+
inbox: queued ? queue : void 0,
|
|
3833
|
+
outbox: queue
|
|
3834
|
+
},
|
|
3835
|
+
manuallyStartQueue: true
|
|
3836
|
+
});
|
|
3837
|
+
const received = [];
|
|
3838
|
+
const contextDocuments = [];
|
|
3839
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, async (ctx, activity) => {
|
|
3840
|
+
received.push(await activity.toJsonLd({ contextLoader: mockDocumentLoader }));
|
|
3841
|
+
assertInstanceOf(ctx, InboxContextImpl);
|
|
3842
|
+
contextDocuments.push(ctx.activity);
|
|
3843
|
+
await ctx.forwardActivity({
|
|
3844
|
+
privateKey: ed25519PrivateKey,
|
|
3845
|
+
keyId: ed25519Multikey.id
|
|
3846
|
+
}, {
|
|
3847
|
+
id: new URL("https://example.com/recipient"),
|
|
3848
|
+
inboxId: new URL("https://example.com/inbox")
|
|
3849
|
+
});
|
|
3850
|
+
});
|
|
3851
|
+
const ctx = federation.createContext(new URL("https://local.example/"));
|
|
3852
|
+
async function processInbox() {
|
|
3853
|
+
const inboxMessages = messages.filter((message) => message.type === "inbox");
|
|
3854
|
+
for (const message of inboxMessages) await federation.processQueuedTask(void 0, message);
|
|
3855
|
+
}
|
|
3856
|
+
assert(await ctx.routeActivity(null, forged));
|
|
3857
|
+
if (queued) {
|
|
3858
|
+
assertEquals(received, []);
|
|
3859
|
+
assertEquals(messages.length, 1);
|
|
3860
|
+
assertEquals(messages[0].activity, genuineJson);
|
|
3861
|
+
await processInbox();
|
|
3862
|
+
}
|
|
3863
|
+
assertEquals(received, [genuineJson]);
|
|
3864
|
+
assertEquals(contextDocuments, [genuineJson]);
|
|
3865
|
+
const forwarded = messages.filter((message) => message.type === "outbox");
|
|
3866
|
+
assertEquals(forwarded.length, 1);
|
|
3867
|
+
assertEquals(forwarded[0].activity, genuineJson);
|
|
3868
|
+
assert(await ctx.routeActivity(null, genuine));
|
|
3869
|
+
if (queued) await processInbox();
|
|
3870
|
+
assertEquals(received, [genuineJson]);
|
|
3871
|
+
assertEquals(contextDocuments, [genuineJson]);
|
|
3872
|
+
assertEquals(messages.filter((message) => message.type === "outbox").length, 1);
|
|
3873
|
+
});
|
|
3673
3874
|
test("ContextImpl.routeActivity() marks queued signed activities as non-LDS", async () => {
|
|
3674
3875
|
let queuedMessage = null;
|
|
3675
3876
|
const federation = new FederationImpl({
|
|
@@ -3729,6 +3930,47 @@ test("ContextImpl.getCollectionUri()", () => {
|
|
|
3729
3930
|
assertThrows(() => ctx.getCollectionUri(Symbol(notReg), values));
|
|
3730
3931
|
assertThrows(() => ctx.getCollectionUri(Symbol.for(notReg), values));
|
|
3731
3932
|
});
|
|
3933
|
+
test("symbol-named custom collections are served and parsed", async () => {
|
|
3934
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
3935
|
+
const names = [Symbol("bookmarks"), Symbol("bookmarks")];
|
|
3936
|
+
const paths = ["bookmarks", "ordered-bookmarks"];
|
|
3937
|
+
const calls = [];
|
|
3938
|
+
federation.setCollectionDispatcher(names[0], vocab.Object, "/users/{identifier}/bookmarks", (_ctx, _values, cursor) => {
|
|
3939
|
+
calls.push([0, cursor]);
|
|
3940
|
+
return { items: [] };
|
|
3941
|
+
});
|
|
3942
|
+
federation.setOrderedCollectionDispatcher(names[1], vocab.Object, "/users/{identifier}/ordered-bookmarks", (_ctx, _values, cursor) => {
|
|
3943
|
+
calls.push([1, cursor]);
|
|
3944
|
+
return { items: [] };
|
|
3945
|
+
});
|
|
3946
|
+
const ctx = federation.createContext(new URL("https://example.com/"));
|
|
3947
|
+
for (const [index, name] of names.entries()) {
|
|
3948
|
+
const uri = ctx.getCollectionUri(name, { identifier: "alice" });
|
|
3949
|
+
assertEquals(uri.pathname, `/users/alice/${paths[index]}`);
|
|
3950
|
+
const parsed = ctx.parseUri(uri);
|
|
3951
|
+
assertEquals(parsed?.type, index === 0 ? "collection" : "orderedCollection");
|
|
3952
|
+
if (parsed?.type !== "collection" && parsed?.type !== "orderedCollection") throw new Error("Expected a custom collection URI");
|
|
3953
|
+
assertStrictEquals(parsed.name, name);
|
|
3954
|
+
assertStrictEquals(parsed.class, vocab.Object);
|
|
3955
|
+
assertEquals(parsed.typeId, vocab.Object.typeId);
|
|
3956
|
+
assertEquals(parsed.values, { identifier: "alice" });
|
|
3957
|
+
for (const cursor of [null, "next"]) {
|
|
3958
|
+
const pageUri = new URL(uri);
|
|
3959
|
+
if (cursor != null) pageUri.searchParams.set("cursor", cursor);
|
|
3960
|
+
const response = await federation.fetch(new Request(pageUri, { headers: { accept: "application/activity+json" } }), { contextData: void 0 });
|
|
3961
|
+
assertEquals(response.status, 200);
|
|
3962
|
+
const body = await response.json();
|
|
3963
|
+
assert(body !== null && typeof body === "object" && "type" in body);
|
|
3964
|
+
assertEquals(body.type, `${index === 0 ? "Collection" : "OrderedCollection"}${cursor == null ? "" : "Page"}`);
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
assertEquals(calls, [
|
|
3968
|
+
[0, null],
|
|
3969
|
+
[0, "next"],
|
|
3970
|
+
[1, null],
|
|
3971
|
+
[1, "next"]
|
|
3972
|
+
]);
|
|
3973
|
+
});
|
|
3732
3974
|
test("InboxContextImpl.forwardActivity()", async (t) => {
|
|
3733
3975
|
esm_default.spyGlobal();
|
|
3734
3976
|
let verified = null;
|