diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts index d4d73217..16ef9302 100644 --- a/packages/browser-sdk/src/client.ts +++ b/packages/browser-sdk/src/client.ts @@ -256,6 +256,13 @@ export type InitOptions = { */ staleTimeMs?: number; + /** + * When proxying requests, you may want to include credentials like cookies + * so you can authorize the request in the proxy. + * This option controls the `credentials` option of the fetch API. + */ + credentials?: "include" | "same-origin" | "omit"; + /** * Base URL of Bucket servers for SSE connections used by AutoFeedback. */ @@ -387,6 +394,7 @@ export class BucketClient { this.httpClient = new HttpClient(this.publishableKey, { baseUrl: this.config.apiBaseUrl, sdkVersion: opts?.sdkVersion, + credentials: opts?.credentials, }); this.featuresClient = new FeaturesClient( @@ -458,7 +466,6 @@ export class BucketClient { } await this.featuresClient.initialize(); - if (this.context.user && this.config.enableTracking) { this.user().catch((e) => { this.logger.error("error sending user", e); diff --git a/packages/browser-sdk/src/httpClient.ts b/packages/browser-sdk/src/httpClient.ts index 43c925f8..5eccca55 100644 --- a/packages/browser-sdk/src/httpClient.ts +++ b/packages/browser-sdk/src/httpClient.ts @@ -3,12 +3,15 @@ import { API_BASE_URL, SDK_VERSION, SDK_VERSION_HEADER_NAME } from "./config"; export interface HttpClientOptions { baseUrl?: string; sdkVersion?: string; + credentials?: RequestCredentials; } export class HttpClient { private readonly baseUrl: string; private readonly sdkVersion: string; + private readonly fetchOptions: RequestInit; + constructor( public publishableKey: string, opts: HttpClientOptions = {}, @@ -21,6 +24,7 @@ export class HttpClient { this.baseUrl += "/"; } this.sdkVersion = opts.sdkVersion ?? SDK_VERSION; + this.fetchOptions = { credentials: opts.credentials }; } getUrl(path: string): URL { @@ -50,13 +54,14 @@ export class HttpClient { url.search = params.toString(); if (timeoutMs === undefined) { - return fetch(url); + return fetch(url, this.fetchOptions); } const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); const res = await fetch(url, { + ...this.fetchOptions, signal: controller.signal, }); clearTimeout(id); @@ -73,6 +78,7 @@ export class HttpClient { body: any; }): ReturnType { return fetch(this.getUrl(path), { + ...this.fetchOptions, method: "POST", headers: { "Content-Type": "application/json", diff --git a/packages/browser-sdk/test/httpClient.test.ts b/packages/browser-sdk/test/httpClient.test.ts index 9996c498..c71a74da 100644 --- a/packages/browser-sdk/test/httpClient.test.ts +++ b/packages/browser-sdk/test/httpClient.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { HttpClient } from "../src/httpClient"; @@ -20,3 +20,44 @@ test.each(cases)("url construction with `path`: %s -> %s", (base, expected) => { const client = new HttpClient("publishableKey", { baseUrl: base }); expect(client.getUrl("path").toString()).toBe(expected); }); + +describe("sets `credentials`", () => { + beforeEach(() => { + vi.spyOn(global, "fetch").mockResolvedValue(new Response()); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + test("default credentials", async () => { + const client = new HttpClient("publishableKey"); + + await client.get({ path: "/test" }); + expect(global.fetch).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ credentials: undefined }), + ); + + await client.post({ path: "/test", body: {} }); + expect(global.fetch).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ credentials: undefined }), + ); + }); + + test("custom credentials", async () => { + const client = new HttpClient("publishableKey", { credentials: "include" }); + + await client.get({ path: "/test" }); + expect(global.fetch).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ credentials: "include" }), + ); + + await client.post({ path: "/test", body: {} }); + expect(global.fetch).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ credentials: "include" }), + ); + }); +}); diff --git a/packages/browser-sdk/test/init.test.ts b/packages/browser-sdk/test/init.test.ts index 5c5b4363..a7c4c191 100644 --- a/packages/browser-sdk/test/init.test.ts +++ b/packages/browser-sdk/test/init.test.ts @@ -17,7 +17,7 @@ const logger = { }; beforeEach(() => { - vi.resetAllMocks(); + vi.clearAllMocks(); }); describe("init", () => { @@ -89,4 +89,19 @@ describe("init", () => { expect(post).not.toHaveBeenCalled(); }); + + test("passes credentials correctly to httpClient", async () => { + const credentials = "include"; + const bucketInstance = new BucketClient({ + publishableKey: KEY, + user: { id: "foo" }, + credentials, + }); + + await bucketInstance.initialize(); + + expect(bucketInstance["httpClient"]["fetchOptions"].credentials).toBe( + credentials, + ); + }); }); diff --git a/vitest.workspace.js b/vitest.workspace.js index 686bcea3..1ff8757f 100644 --- a/vitest.workspace.js +++ b/vitest.workspace.js @@ -6,6 +6,4 @@ export default defineWorkspace([ "./packages/openfeature-node-provider/vite.config.js", "./packages/node-sdk/vite.config.js", "./packages/react-sdk/vite.config.mjs", - "./packages/tracking-sdk/vite.e2e.config.js", - "./packages/tracking-sdk/vite.config.js", ]);