Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 28 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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);
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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 ===
Expand Down
49 changes: 46 additions & 3 deletions tests/forwardRequest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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");
});
});
10 changes: 10 additions & 0 deletions tests/getVersion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down
6 changes: 3 additions & 3 deletions vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
Expand Down