From 94559aa9a2acad128c68fc5fd4a859a695ca25bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:40:35 +0000 Subject: [PATCH 1/4] Initial plan From c194795f7364b91093580c91a791e0e6af520f48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:59:05 +0000 Subject: [PATCH 2/4] Add KeetaNetCombinedAnchorHTTPServer for combining routes from multiple servers Agent-Logs-Url: https://github.com/KeetaNetwork/anchor/sessions/adb8fb18-e173-4efe-b121-250f7a36612b Co-authored-by: ezraripps <19670988+ezraripps@users.noreply.github.com> --- src/lib/http-server/index.test.ts | 114 ++++++++++++++++++++++++++++++ src/lib/http-server/index.ts | 102 ++++++++++++++++++++------ 2 files changed, 193 insertions(+), 23 deletions(-) diff --git a/src/lib/http-server/index.test.ts b/src/lib/http-server/index.test.ts index ace6f5a9..094b4603 100644 --- a/src/lib/http-server/index.test.ts +++ b/src/lib/http-server/index.test.ts @@ -589,3 +589,117 @@ test('maxBodySize: rejects oversized requests', async function() { await server.stop(); }, 30000); + +test('KeetaNetCombinedAnchorHTTPServer: combines routes from multiple servers', async function() { + const serverA = new (class extends HTTPServer.KeetaNetAnchorHTTPServer { + protected async initRoutes(): Promise { + return({ + 'GET /api/server-a': async function() { + return({ output: JSON.stringify({ server: 'A' }), statusCode: 200 }); + } + }); + } + })({ port: 0 }); + + const serverB = new (class extends HTTPServer.KeetaNetAnchorHTTPServer { + protected async initRoutes(): Promise { + return({ + 'GET /api/server-b': async function() { + return({ output: JSON.stringify({ server: 'B' }), statusCode: 200 }); + } + }); + } + })({ port: 0 }); + + await using combined = new HTTPServer.KeetaNetCombinedAnchorHTTPServer({ + port: 0, + servers: [serverA, serverB] + }); + + await combined.start(); + + /* + * Routes from both child servers should be accessible via the combined server. + */ + const responseA = await testHTTPRequest(combined.url, '/api/server-a', 'GET'); + expect(responseA.code).toBe(200); + expect(responseA.body).toMatchObject({ server: 'A' }); + + const responseB = await testHTTPRequest(combined.url, '/api/server-b', 'GET'); + expect(responseB.code).toBe(200); + expect(responseB.body).toMatchObject({ server: 'B' }); + + /* + * A route that doesn't exist on either child should return 404. + */ + const responseMissing = await testHTTPRequest(combined.url, '/api/missing', 'GET'); + expect(responseMissing.code).toBe(404); + + /* + * The combined server's URL should be propagated to child instances after start. + */ + expect(serverA.url).toBe(combined.url); + expect(serverB.url).toBe(combined.url); +}, 30000); + +test('KeetaNetCombinedAnchorHTTPServer: later child routes overwrite earlier ones on conflict', async function() { + const serverA = new (class extends HTTPServer.KeetaNetAnchorHTTPServer { + protected async initRoutes(): Promise { + return({ + 'GET /api/conflict': async function() { + return({ output: JSON.stringify({ from: 'A' }), statusCode: 200 }); + } + }); + } + })({ port: 0 }); + + const serverB = new (class extends HTTPServer.KeetaNetAnchorHTTPServer { + protected async initRoutes(): Promise { + return({ + 'GET /api/conflict': async function() { + return({ output: JSON.stringify({ from: 'B' }), statusCode: 200 }); + } + }); + } + })({ port: 0 }); + + await using combined = new HTTPServer.KeetaNetCombinedAnchorHTTPServer({ + port: 0, + servers: [serverA, serverB] + }); + + await combined.start(); + + const response = await testHTTPRequest(combined.url, '/api/conflict', 'GET'); + expect(response.code).toBe(200); + expect(response.body).toMatchObject({ from: 'B' }); +}, 30000); + +test('KeetaNetCombinedAnchorHTTPServer: url set on combined server propagates to children', async function() { + const child = new (class extends HTTPServer.KeetaNetAnchorHTTPServer { + protected async initRoutes(): Promise { + return({ + 'GET /health': async function() { + return({ output: JSON.stringify({ ok: true }), statusCode: 200 }); + } + }); + } + })({ port: 0 }); + + const customURL = 'https://anchor.example.com/'; + + await using combined = new HTTPServer.KeetaNetCombinedAnchorHTTPServer({ + port: 0, + url: customURL, + servers: [child] + }); + + await combined.start(); + + expect(combined.url).toBe(customURL); + + /* + * The custom URL should also be propagated to the child. + */ + expect(child.url).toBe(customURL); +}, 30000); diff --git a/src/lib/http-server/index.ts b/src/lib/http-server/index.ts index 86c6ddeb..4f36c8ae 100644 --- a/src/lib/http-server/index.ts +++ b/src/lib/http-server/index.ts @@ -69,7 +69,7 @@ export abstract class KeetaNetAnchorHTTPServer; #server?: http.Server; #urlParts: undefined | { hostname?: string; port?: number; protocol?: string; }; - #url: undefined | string | URL | ((object: this) => string); + #url: undefined | string | URL | ((object: KeetaNetAnchorHTTPServer) => string); readonly #config: ConfigType; constructor(config: ConfigType) { @@ -83,18 +83,6 @@ export abstract class KeetaNetAnchorHTTPServer string)) { + set url(value: undefined | string | URL | ((object: KeetaNetAnchorHTTPServer) => string)) { this.#urlParts = undefined; this.#url = value; } + /** + * Build the routes for this server by calling initRoutes with the + * config that was provided at construction time. This is used + * internally by KeetaNetCombinedAnchorHTTPServer to gather routes + * from each child server. + */ + protected buildRoutes(): Promise { + return(this.initRoutes(this.#config)); + } + + /** + * Static helper that allows KeetaNetCombinedAnchorHTTPServer (and other + * subclasses) to invoke buildRoutes() on an arbitrary + * KeetaNetAnchorHTTPServer instance. Protected access is valid here + * because we are inside the KeetaNetAnchorHTTPServer class body. + */ + protected static callBuildRoutes(server: KeetaNetAnchorHTTPServer): Promise { + return(server.buildRoutes()); + } + [Symbol.asyncDispose](): Promise { return(this.stop()); } } + +export interface KeetaAnchorCombinedHTTPServerConfig extends KeetaAnchorHTTPServerConfig { + /** + * The list of servers whose routes should be combined. + */ + servers: KeetaNetAnchorHTTPServer[]; +} + +/** + * A KeetaNetAnchorHTTPServer that combines the routes of multiple + * KeetaNetAnchorHTTPServer instances into a single HTTP server. + * + * When the combined server starts it propagates its URL to all child + * server instances so that callers holding references to those children + * can retrieve the correct URL via child.url. + */ +export class KeetaNetCombinedAnchorHTTPServer extends KeetaNetAnchorHTTPServer { + readonly #children: KeetaNetAnchorHTTPServer[]; + + constructor(config: KeetaAnchorCombinedHTTPServerConfig) { + super(config); + this.#children = [...config.servers]; + } + + protected async initRoutes(_ignoreConfig: KeetaAnchorCombinedHTTPServerConfig): Promise { + const combined: Routes = {}; + + for (const child of this.#children) { + const childRoutes: Routes = await KeetaNetCombinedAnchorHTTPServer.callBuildRoutes(child); + Object.assign(combined, childRoutes); + } + + return(combined); + } + + override async start(): Promise { + await super.start(); + + /* + * Propagate the combined server's URL to all child server + * instances so that code holding a reference to a child can + * call child.url and receive the correct address. + */ + const url = this.url; + for (const child of this.#children) { + child.url = url; + } + } +} From ac464cbcd71568388193196563bbf8c7405951df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:13:48 +0000 Subject: [PATCH 3/4] Address review feedback: remove callBuildRoutes, use config in initRoutes, validate child URLs Agent-Logs-Url: https://github.com/KeetaNetwork/anchor/sessions/f2463b59-588b-4959-99c0-b9e5ac7293c2 Co-authored-by: ezraripps <19670988+ezraripps@users.noreply.github.com> --- src/lib/http-server/index.test.ts | 24 +++++++++++++++++++ src/lib/http-server/index.ts | 39 +++++++++++++++++-------------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/lib/http-server/index.test.ts b/src/lib/http-server/index.test.ts index 094b4603..9f3b60ea 100644 --- a/src/lib/http-server/index.test.ts +++ b/src/lib/http-server/index.test.ts @@ -703,3 +703,27 @@ test('KeetaNetCombinedAnchorHTTPServer: url set on combined server propagates to */ expect(child.url).toBe(customURL); }, 30000); + +test('KeetaNetCombinedAnchorHTTPServer: throws when a child has a conflicting URL', async function() { + const conflictingURL = 'https://other.example.com/'; + + const child = new (class extends HTTPServer.KeetaNetAnchorHTTPServer { + protected async initRoutes(): Promise { + return({ + 'GET /health': async function() { + return({ output: JSON.stringify({ ok: true }), statusCode: 200 }); + } + }); + } + })({ port: 0, url: conflictingURL }); + + const customURL = 'https://anchor.example.com/'; + + await using combined = new HTTPServer.KeetaNetCombinedAnchorHTTPServer({ + port: 0, + url: customURL, + servers: [child] + }); + + await expect(combined.start()).rejects.toThrow(`Child server url "${conflictingURL}" does not match combined server url "${customURL}"`); +}, 30000); diff --git a/src/lib/http-server/index.ts b/src/lib/http-server/index.ts index 4f36c8ae..ef3ba6e4 100644 --- a/src/lib/http-server/index.ts +++ b/src/lib/http-server/index.ts @@ -690,16 +690,6 @@ export abstract class KeetaNetAnchorHTTPServer { - return(server.buildRoutes()); - } - [Symbol.asyncDispose](): Promise { return(this.stop()); } @@ -728,11 +718,14 @@ export class KeetaNetCombinedAnchorHTTPServer extends KeetaNetAnchorHTTPServer { + protected async initRoutes(config: KeetaAnchorCombinedHTTPServerConfig): Promise { const combined: Routes = {}; - for (const child of this.#children) { - const childRoutes: Routes = await KeetaNetCombinedAnchorHTTPServer.callBuildRoutes(child); + for (const child of config.servers) { + // buildRoutes() is protected; bracket notation is used here to access + // it on an arbitrary KeetaNetAnchorHTTPServer instance. + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const childRoutes: Routes = await (child as unknown as { buildRoutes(): Promise }).buildRoutes(); Object.assign(combined, childRoutes); } @@ -742,13 +735,25 @@ export class KeetaNetCombinedAnchorHTTPServer extends KeetaNetAnchorHTTPServer { await super.start(); + const url = this.url; + /* - * Propagate the combined server's URL to all child server - * instances so that code holding a reference to a child can - * call child.url and receive the correct address. + * Propagate the combined server's URL to all child server instances. + * If a child already has a URL set it must match the combined server's + * URL; a mismatch indicates a misconfiguration. */ - const url = this.url; for (const child of this.#children) { + let childURL: string | undefined; + try { + childURL = child.url; + } catch { + /* child has no static URL set; the URL will be propagated below */ + } + + if (childURL !== undefined && childURL !== url) { + throw(new Error(`Child server url "${childURL}" does not match combined server url "${url}"`)); + } + child.url = url; } } From 3973da71aa0c5839f6ceb90406c5657ddd1e8890 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:05:44 +0000 Subject: [PATCH 4/4] Remove #children field, add protected config getter, fix bare catch, revert cosmetic url changes Agent-Logs-Url: https://github.com/KeetaNetwork/anchor/sessions/0bd3f93f-6749-464f-867d-98a392a18242 Co-authored-by: ezraripps <19670988+ezraripps@users.noreply.github.com> --- src/lib/http-server/index.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/lib/http-server/index.ts b/src/lib/http-server/index.ts index ef3ba6e4..2ee72b5f 100644 --- a/src/lib/http-server/index.ts +++ b/src/lib/http-server/index.ts @@ -658,7 +658,9 @@ export abstract class KeetaNetAnchorHTTPServer string)) { @@ -680,6 +684,14 @@ export abstract class KeetaNetAnchorHTTPServer { - readonly #children: KeetaNetAnchorHTTPServer[]; - - constructor(config: KeetaAnchorCombinedHTTPServerConfig) { - super(config); - this.#children = [...config.servers]; - } - protected async initRoutes(config: KeetaAnchorCombinedHTTPServerConfig): Promise { const combined: Routes = {}; @@ -742,11 +747,14 @@ export class KeetaNetCombinedAnchorHTTPServer extends KeetaNetAnchorHTTPServer