diff --git a/package-lock.json b/package-lock.json index a3e98ed..c7bef64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1242,6 +1242,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2025,6 +2026,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4650,6 +4652,7 @@ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4733,6 +4736,7 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", diff --git a/src/index.js b/src/index.js index a1274c4..8d77031 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,8 @@ export const CONFIG = { USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", MIN_VERSION: "0.23.0", - SERVERLESS_VERSION: "2025-10-20", - NODES: ["https://api.justyy.com", "https://api.steemit.com"], + SERVERLESS_VERSION: "2026-01-12", + NODES: ["https://api2.justyy.com", "https://api.steemit.com"], FETCH_TIMEOUT_MS: 5000, }; @@ -14,6 +14,15 @@ export const corsHeaders = { "Access-Control-Allow-Headers": "Content-Type", }; +export const DOWNSTREAM_HEADERS = Object.freeze({ + "https://api.steemit.com": { + "X-Edge-Key": "static_secret_value_here", + }, + "https://api2.justyy.com": { + "X-Edge-Key": "another_static_secret_value", + }, +}); + // === Utilities === export const compareVersion = (v1, v2) => { const a = v1.split(".").map(Number); @@ -75,13 +84,20 @@ export async function safeGetVersion(server, _fetchWithTimeout) { } } -export async function forwardRequest(apiURL, body = null, method = "GET", _fetchWithTimeout) { +export async function forwardRequest( + apiURL, + body = null, + method = "GET", + extraHeaders = {}, + _fetchWithTimeout +) { const fetcher = _fetchWithTimeout || fetchWithTimeout; const res = await fetcher(apiURL, { method, headers: { "Content-Type": "application/json", "User-Agent": CONFIG.USER_AGENT, + ...extraHeaders, }, body: body ? JSON.stringify(body) : null, }); @@ -116,11 +132,18 @@ export default { }); // === Forward the actual request === let respObj; + + if (!DOWNSTREAM_HEADERS[selected.server]) { + throw new Error(`No security headers defined for ${selected.server}`); + } + + const nodeHeaders = DOWNSTREAM_HEADERS[selected.server]; + if (method === "POST") { const body = await request.json(); - respObj = await forwardRequest(selected.server, body, "POST"); + respObj = await forwardRequest(selected.server, body, "POST", nodeHeaders); } else { - respObj = await forwardRequest(selected.server); + respObj = await forwardRequest(selected.server, null, "GET", nodeHeaders); } // === Parse upstream response === diff --git a/tests/forwardRequest.test.js b/tests/forwardRequest.test.js index 6ce5837..ffc7620 100644 --- a/tests/forwardRequest.test.js +++ b/tests/forwardRequest.test.js @@ -9,7 +9,13 @@ describe("forwardRequest()", () => { text: async () => '{"message":"ok"}', }); - const result = await forwardRequest("https://example.com", null, "GET", mockFetch); + const result = await forwardRequest( + "https://example.com", + null, + "GET", + {}, // extraHeaders + mockFetch // fetchFn + ); expect(mockFetch).toHaveBeenCalledOnce(); expect(mockFetch).toHaveBeenCalledWith( @@ -27,22 +33,59 @@ describe("forwardRequest()", () => { text: async () => '{"success":true}', }); - const result = await forwardRequest("https://example.com/api", body, "POST", mockFetch); + const result = await forwardRequest( + "https://example.com/api", + body, + "POST", + {}, // extraHeaders + mockFetch // fetchFn + ); expect(mockFetch).toHaveBeenCalledOnce(); + const callOptions = mockFetch.mock.calls[0][1]; expect(callOptions.method).toBe("POST"); expect(callOptions.body).toBe(JSON.stringify(body)); + expect(result).toEqual({ statusCode: 201, text: '{"success":true}' }); }); + it("forwards extra headers to downstream", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => "{}", + }); + + const extraHeaders = { + "X-Edge-Key": "TEST_SECRET", + }; + + await forwardRequest("https://example.com", null, "GET", extraHeaders, mockFetch); + + const callOptions = mockFetch.mock.calls[0][1]; + expect(callOptions.headers["X-Edge-Key"]).toBe("TEST_SECRET"); + }); + it("throws if fetchFn rejects", async () => { const mockFetch = vi.fn().mockRejectedValue(new Error("Network Error")); - await expect(forwardRequest("https://example.com", null, "GET", mockFetch)).rejects.toThrow( + await expect(forwardRequest("https://example.com", null, "GET", {}, mockFetch)).rejects.toThrow( "Network Error" ); expect(mockFetch).toHaveBeenCalledOnce(); }); + + it("handles non-JSON upstream response", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => "NOT_JSON", + }); + + const result = await forwardRequest("https://example.com", null, "GET", {}, mockFetch); + + expect(result.text).toBe("NOT_JSON"); + }); }); diff --git a/tests/getVersion.test.js b/tests/getVersion.test.js index c392a1d..ed32bca 100644 --- a/tests/getVersion.test.js +++ b/tests/getVersion.test.js @@ -52,6 +52,16 @@ describe("getVersion() and safeGetVersion()", () => { expect(version).toEqual({ server: "https://example.com", version: "2.0.0" }); }); + it("safeGetVersion returns version if getVersion returns version that is too old", async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ result: { blockchain_version: "0.0.0" } }), + }); + + const version = safeGetVersion("https://example.com", mockFetch); + await expect(version).rejects.toThrow("Version too low: 0.0.0"); + }); + it("safeGetVersion logs warning and rethrows if getVersion fails", async () => { mockFetch.mockResolvedValue({ ok: false, status: 500 }); diff --git a/vitest.config.js b/vitest.config.js index cd9389f..2a152af 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -7,10 +7,10 @@ export default defineConfig({ reporter: ['text', 'html', 'json', 'json-summary'], reportsDirectory: './coverage', thresholds: { - lines: 96, - statements: 96, + lines: 96.55, + statements: 96.55, functions: 100, - branches: 88, + branches: 89.13, }, }, },