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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
[ZeroBounce](https://www.zerobounce.net) JavaScript API v2


## Security

- Keep API keys on a trusted server. Do not embed them in mobile apps or browser JavaScript that untrusted users can inspect.
- Custom API base URLs (when supported) must use `https://`. Do not pass end-user-controlled hosts into those settings.
- Request URLs include `api_key` as a query parameter (ZeroBounce API contract). Do not log full request URLs or enable payload debug logging in production.

## WE DO NOT RECOMMEND USING THIS SDK ON A FRONT-END ENVIRONMENT AS THE API KEY WILL BE VISIBLE

This is a JavaScript wrapper class for the ZeroBounce API v2.
Expand Down
17 changes: 17 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
// Constants
export const API_BULK_BASE_URL = "https://bulkapi.zerobounce.net/v2";
export const HTTP_TIMEOUT_MS = 120000;
export const HEADERS = {
Accept: "*/*",
"Accept-Encoding": "gzip, deflate, br",
Connection: "keep-alive",
};

export function requireHttpsUrl(url) {
if (typeof url !== "string" || !/^https:\/\//i.test(url)) {
parameterIsInvalid("apiBaseURL", "Must be an https:// URL.");
throw new Error("apiBaseURL must be an https:// URL");
}
return url;
}

function fetchTimeoutSignal() {
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
return AbortSignal.timeout(HTTP_TIMEOUT_MS);
}
return undefined;
}

function nonEmptyStringField(o, key) {
const v = o[key];
if (v == null) return false;
Expand Down Expand Up @@ -100,6 +116,7 @@ export async function createRequest({
method: requestType,
headers: HEADERS,
body,
signal: fetchTimeoutSignal(),
});
if (returnText) {
const finalResult = await response.text();
Expand Down
4 changes: 2 additions & 2 deletions src/zero-bounce.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createRequest, notInitialized, parameterIsMissing, parameterIsInvalid } from "./utils.js";
import { createRequest, notInitialized, parameterIsMissing, parameterIsInvalid, requireHttpsUrl } from "./utils.js";

/**
* Validation status values returned by the API (validate, validateBatch).
Expand Down Expand Up @@ -94,7 +94,7 @@ export class ZeroBounceSDK {
parameterIsMissing("Api key", "Please provide a valid API key.");
} else {
this._api_key = apiKey;
this._api_base_url = apiBaseURL;
this._api_base_url = requireHttpsUrl(apiBaseURL);
this._initialized = true;
}
}
Expand Down
8 changes: 8 additions & 0 deletions tests/zero-bounce.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ describe("ZeroBounceSDK", () => {
});
});

describe("init security", () => {
it("rejects a non-https apiBaseURL", () => {
expect(() => zeroBounceSDK.init("valid-api-key", "http://evil.example/v2")).toThrow(
"apiBaseURL must be an https:// URL"
);
});
});

describe("getCredits", () => {
it("should throw an error if not initialized", async () => {
await zeroBounceSDK.getCredits();
Expand Down