From 0cf1c56d6ec43994a68b5b083ad95dde4149a978 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Thu, 9 Apr 2026 18:11:43 +0100 Subject: [PATCH 1/5] feat: always add API key to endpoint requests --- src/__tests__/client.test.js | 51 ++++++------------------------------ src/client.ts | 25 +++++++----------- 2 files changed, 18 insertions(+), 58 deletions(-) diff --git a/src/__tests__/client.test.js b/src/__tests__/client.test.js index 7834ff4..921241d 100644 --- a/src/__tests__/client.test.js +++ b/src/__tests__/client.test.js @@ -197,6 +197,14 @@ describe("Client", () => { expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith(`${endpoint}/context?application=test_app&environment=test`, { method: "GET", + headers: { + "Content-Type": "application/json", + "X-API-Key": apiKey, + "X-Agent": "javascript-client", + "X-Environment": "test", + "X-Application": "test_app", + "X-Application-Version": 1000000, + }, keepalive: true, signal: expect.any(Object), }); @@ -217,7 +225,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -266,7 +273,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -310,7 +316,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -361,7 +366,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -402,7 +406,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -428,7 +431,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -455,7 +457,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -497,7 +498,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -532,7 +532,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -562,7 +561,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1 }, @@ -587,7 +585,6 @@ describe("Client", () => { client .request({ - auth: true, method: "POST", path: "/context", query: { a: 1 }, @@ -625,7 +622,6 @@ describe("Client", () => { client .request({ - auth: true, method: "POST", path: "/context", query: { a: 1 }, @@ -663,7 +659,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: { a: 1, b: "ã=á" }, @@ -699,7 +694,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", query: {}, @@ -735,7 +729,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", }) @@ -769,7 +762,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", }) @@ -796,32 +788,6 @@ describe("Client", () => { }); }); - it("request() should not send headers when auth argument is false", (done) => { - fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); - - const client = new Client(Object.assign({}, clientOptions, { application: "website" })); - - client - .request({ - auth: false, - method: "PUT", - path: "/context", - }) - .then((response) => { - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenLastCalledWith(`${endpoint}/context`, { - method: "PUT", - body: undefined, - keepalive: true, - signal: expect.any(Object), - }); - - expect(response).toEqual(defaultMockResponse); - - done(); - }); - }); - it("publish() calls endpoint", (done) => { fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); @@ -954,7 +920,6 @@ describe("Client", () => { client .request({ - auth: true, method: "PUT", path: "/context", }) diff --git a/src/client.ts b/src/client.ts index e7d4e12..b4746aa 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,7 +21,6 @@ export type ClientRequestOptions = { path: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"; body?: Record; - auth?: boolean; signal?: AbortSignal | ABsmartlyAbortSignal; timeout?: number; }; @@ -90,7 +89,7 @@ export default class Client { } getContext(options?: Partial) { - return this.getUnauthed({ + return this.get({ ...options, path: "/context", query: { @@ -161,16 +160,14 @@ export default class Client { keepalive: this._opts.keepalive, }; - if (options.auth) { - opts.headers = { - "Content-Type": "application/json", - "X-API-Key": this._opts.apiKey, - "X-Agent": this._opts.agent, - "X-Environment": this._opts.environment, - "X-Application": this._opts.application.name, - "X-Application-Version": this._opts.application.version, - }; - } + opts.headers = { + "Content-Type": "application/json", + "X-API-Key": this._opts.apiKey, + "X-Agent": this._opts.agent, + "X-Environment": this._opts.environment, + "X-Application": this._opts.application.name, + "X-Application-Version": this._opts.application.version, + }; return fetch(url, opts).then((response: FetchResponse) => { if (!response.ok) { @@ -281,7 +278,6 @@ export default class Client { post(options: ClientRequestOptions) { return this.request({ ...options, - auth: true, method: "POST", }); } @@ -289,7 +285,6 @@ export default class Client { put(options: ClientRequestOptions) { return this.request({ ...options, - auth: true, method: "PUT", }); } @@ -302,7 +297,7 @@ export default class Client { return this._opts.application; } - getUnauthed(options: ClientRequestOptions) { + get(options: ClientRequestOptions) { return this.request({ ...options, method: "GET", From 3c0b095b09bb6b7e1a7b6b7cb533116bed9401e1 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Thu, 9 Apr 2026 18:11:59 +0100 Subject: [PATCH 2/5] fix: deprecate auth flag --- src/__tests__/client.test.js | 26 ++++++++++++++++++++++++++ src/client.ts | 20 ++++++++++++-------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/__tests__/client.test.js b/src/__tests__/client.test.js index 921241d..9a3dba2 100644 --- a/src/__tests__/client.test.js +++ b/src/__tests__/client.test.js @@ -788,6 +788,32 @@ describe("Client", () => { }); }); + it("request() should not send headers when auth argument is false", (done) => { + fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + + const client = new Client(Object.assign({}, clientOptions, { application: "website" })); + + client + .request({ + auth: false, + method: "PUT", + path: "/context", + }) + .then((response) => { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith(`${endpoint}/context`, { + method: "PUT", + body: undefined, + keepalive: true, + signal: expect.any(Object), + }); + + expect(response).toEqual(defaultMockResponse); + + done(); + }); + }); + it("publish() calls endpoint", (done) => { fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); diff --git a/src/client.ts b/src/client.ts index b4746aa..9409711 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,6 +21,8 @@ export type ClientRequestOptions = { path: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"; body?: Record; + /** @deprecated The API key is now always sent. This option will be removed in a future version. */ + auth?: boolean; signal?: AbortSignal | ABsmartlyAbortSignal; timeout?: number; }; @@ -160,14 +162,16 @@ export default class Client { keepalive: this._opts.keepalive, }; - opts.headers = { - "Content-Type": "application/json", - "X-API-Key": this._opts.apiKey, - "X-Agent": this._opts.agent, - "X-Environment": this._opts.environment, - "X-Application": this._opts.application.name, - "X-Application-Version": this._opts.application.version, - }; + if (options.auth !== false) { + opts.headers = { + "Content-Type": "application/json", + "X-API-Key": this._opts.apiKey, + "X-Agent": this._opts.agent, + "X-Environment": this._opts.environment, + "X-Application": this._opts.application.name, + "X-Application-Version": this._opts.application.version, + }; + } return fetch(url, opts).then((response: FetchResponse) => { if (!response.ok) { From cabcd9c42d09012edf24b3ae292e4b89ad7dffd2 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Thu, 9 Apr 2026 18:12:03 +0100 Subject: [PATCH 3/5] test: add tests for GET request headers and explicit auth = true --- src/__tests__/client.test.js | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/__tests__/client.test.js b/src/__tests__/client.test.js index 9a3dba2..eed8a1d 100644 --- a/src/__tests__/client.test.js +++ b/src/__tests__/client.test.js @@ -814,6 +814,75 @@ describe("Client", () => { }); }); + it("request() should still send headers when auth is explicitly true (deprecated)", (done) => { + fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + + const client = new Client(clientOptions); + + client + .request({ + auth: true, + method: "PUT", + path: "/context", + }) + .then((response) => { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith(`${endpoint}/context`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-Key": apiKey, + "X-Agent": "javascript-client", + "X-Environment": "test", + "X-Application": "test_app", + "X-Application-Version": 1000000, + }, + body: undefined, + keepalive: true, + signal: expect.any(Object), + }); + + expect(response).toEqual(defaultMockResponse); + + done(); + }); + }); + + it("get() should send a GET request with headers", (done) => { + fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + + const client = new Client(clientOptions); + + client + .get({ + path: "/context", + query: { application: "test_app", environment: "test" }, + }) + .then((response) => { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + `${endpoint}/context?application=test_app&environment=test`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-API-Key": apiKey, + "X-Agent": "javascript-client", + "X-Environment": "test", + "X-Application": "test_app", + "X-Application-Version": 1000000, + }, + keepalive: true, + signal: expect.any(Object), + } + ); + + expect(response).toEqual(defaultMockResponse); + + done(); + }); + }); + it("publish() calls endpoint", (done) => { fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); From 202d19843f24caa2d0e80fb82130533b34a80ff8 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Mon, 13 Apr 2026 14:32:27 +0100 Subject: [PATCH 4/5] docs: correct auth deprecation comment to match runtime behavior --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 9409711..e4680ad 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,7 +21,7 @@ export type ClientRequestOptions = { path: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"; body?: Record; - /** @deprecated The API key is now always sent. This option will be removed in a future version. */ + /** @deprecated The API key is sent by default; set to false to suppress auth headers. */ auth?: boolean; signal?: AbortSignal | ABsmartlyAbortSignal; timeout?: number; From a9cd82cec8b2b3542e5e934413841c8ba7f4d0c3 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Mon, 13 Apr 2026 16:43:21 +0100 Subject: [PATCH 5/5] chore: review comments --- src/__tests__/client.test.js | 69 ++++++++++++++++++++++++++++++++++++ src/client.ts | 11 ++++++ 2 files changed, 80 insertions(+) diff --git a/src/__tests__/client.test.js b/src/__tests__/client.test.js index eed8a1d..80cf106 100644 --- a/src/__tests__/client.test.js +++ b/src/__tests__/client.test.js @@ -816,6 +816,7 @@ describe("Client", () => { it("request() should still send headers when auth is explicitly true (deprecated)", (done) => { fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + const warnSpy = jest.spyOn(console, "warn").mockImplementation(); const client = new Client(clientOptions); @@ -842,8 +843,13 @@ describe("Client", () => { signal: expect.any(Object), }); + expect(warnSpy).toHaveBeenCalledWith( + "[ABsmartly] The `auth` option is deprecated. Auth headers are now sent by default. Remove `auth: true` from your request options." + ); + expect(response).toEqual(defaultMockResponse); + warnSpy.mockRestore(); done(); }); }); @@ -883,6 +889,69 @@ describe("Client", () => { }); }); + it("get() should not send headers when auth is false", (done) => { + fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + + const client = new Client(Object.assign({}, clientOptions, { application: "website" })); + + client + .get({ + auth: false, + path: "/context", + query: { application: "website", environment: "test" }, + }) + .then((response) => { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + `${endpoint}/context?application=website&environment=test`, + { + method: "GET", + keepalive: true, + signal: expect.any(Object), + } + ); + + expect(response).toEqual(defaultMockResponse); + + done(); + }); + }); + + it("getUnauthed() should forward to get()", (done) => { + fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + + const client = new Client(clientOptions); + + client + .getUnauthed({ + path: "/context", + query: { application: "test_app", environment: "test" }, + }) + .then((response) => { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + `${endpoint}/context?application=test_app&environment=test`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-API-Key": apiKey, + "X-Agent": "javascript-client", + "X-Environment": "test", + "X-Application": "test_app", + "X-Application-Version": 1000000, + }, + keepalive: true, + signal: expect.any(Object), + } + ); + + expect(response).toEqual(defaultMockResponse); + + done(); + }); + }); + it("publish() calls endpoint", (done) => { fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); diff --git a/src/client.ts b/src/client.ts index e4680ad..56d91a2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -162,6 +162,12 @@ export default class Client { keepalive: this._opts.keepalive, }; + if (options.auth === true) { + console.warn( + "[ABsmartly] The `auth` option is deprecated. Auth headers are now sent by default. Remove `auth: true` from your request options." + ); + } + if (options.auth !== false) { opts.headers = { "Content-Type": "application/json", @@ -307,4 +313,9 @@ export default class Client { method: "GET", }); } + + /** @deprecated Use get() instead. */ + getUnauthed(options: ClientRequestOptions) { + return this.get(options); + } }