From bc924da633b2b36eeca8eed2300b44c7316a5a7d Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:36:14 -0700 Subject: [PATCH 1/2] feat: expose `fetch` option on `ManagementClient` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1330. The internal fetcher layer added `BaseClientOptions.fetch?: typeof fetch` in #1238 and routes it through `fetcherImpl(args.fetchFn ?? …)`. The public wrapper, however, both omitted `fetch` from `ManagementClientOptions` and explicitly deleted it from the incoming options bag, so library consumers had no supported way to provide their own HTTP transport — even though the SDK is already capable of using one. - Drop `"fetch"` from the `Omit` list so the field is part of the public surface. - Stop deleting `_options.fetch` in the `ManagementClient` constructor. The internal `fetcher` is still stripped (it's a Fern implementation detail), with a comment explaining why. Adds a focused test in `tests/management/ManagementClient.test.ts` that constructs a `ManagementClient` with a custom `fetch`, fires a request, and asserts the custom implementation was invoked against the expected URL. Also covers the no-`fetch` happy path to make sure the default global-fetch route is unchanged. --- src/management/wrapper/ManagementClient.ts | 14 +++++-- tests/management/ManagementClient.test.ts | 46 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tests/management/ManagementClient.test.ts diff --git a/src/management/wrapper/ManagementClient.ts b/src/management/wrapper/ManagementClient.ts index ba96160d73..5d852f2b73 100644 --- a/src/management/wrapper/ManagementClient.ts +++ b/src/management/wrapper/ManagementClient.ts @@ -22,8 +22,10 @@ export declare namespace ManagementClient { * @group Management API * @public */ - export interface ManagementClientOptions - extends Omit { + export interface ManagementClientOptions extends Omit< + FernClient.Options, + "token" | "environment" | "fetcher" | "baseUrl" + > { /** Auth0 domain (e.g., 'your-tenant.auth0.com') */ domain: string; /** @@ -206,9 +208,13 @@ export class ManagementClient extends FernClient { const headers = createTelemetryHeaders(_options); const token = createTokenSupplier(_options); - // Temporarily remove fetcher from options to avoid people passing it for now + // The underlying fetcher type is internal to the generated Fern client + // and is intentionally kept off the public surface. `fetch`, however, + // is supported all the way down through `fetcherImpl` and is a + // legitimate customization hook for callers that need to control the + // HTTP transport (proxies, retries, instrumentation). + // https://github.com/auth0/node-auth0/issues/1330 delete (_options as any).fetcher; - delete (_options as any).fetch; // Prepare the base client options let clientOptions: any = { diff --git a/tests/management/ManagementClient.test.ts b/tests/management/ManagementClient.test.ts new file mode 100644 index 0000000000..3df660d74d --- /dev/null +++ b/tests/management/ManagementClient.test.ts @@ -0,0 +1,46 @@ +import { ManagementClient } from "../../src/management/index.js"; + +describe("ManagementClient (wrapper)", () => { + describe("custom fetch option", () => { + // Regression for https://github.com/auth0/node-auth0/issues/1330 + // The wrapper used to `delete (_options as any).fetch`, which silently + // dropped any caller-provided custom fetch implementation even though + // the underlying `fetcherImpl` honours `fetchFn`. Verify the wrapper + // now forwards `fetch` all the way through. + it("should invoke the provided custom fetch implementation", async () => { + const calls: { url: string; init?: RequestInit }[] = []; + const customFetch: typeof fetch = (async (input, init) => { + const url = typeof input === "string" ? input : (input as URL | Request).toString(); + calls.push({ url, init }); + return new Response(JSON.stringify([{ id: "rul_test", name: "Test Rule" }]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof fetch; + + const client = new ManagementClient({ + domain: "tenant.auth0.com", + token: "test-token", + fetch: customFetch, + }); + + await client.rules.list().catch(() => { + // Iterator parsing may not match the mocked payload shape; the + // assertion below only cares that the custom fetch was invoked. + }); + + expect(calls.length).toBeGreaterThanOrEqual(1); + expect(calls[0].url).toContain("tenant.auth0.com/api/v2/rules"); + }); + + it("should not throw when the `fetch` option is omitted (defaults to global fetch)", () => { + expect( + () => + new ManagementClient({ + domain: "tenant.auth0.com", + token: "test-token", + }), + ).not.toThrow(); + }); + }); +}); From ad613191a578fcebd1e2962b22db8230b1ea55f5 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 16 Jul 2026 13:49:21 +0530 Subject: [PATCH 2/2] test: move fetch test to unit suite; enrich JSDoc and fix example Follow-ups on top of the custom-fetch fix: - Move the wrapper test from tests/management/ into the unit suite (src/management/tests/unit/) alongside the other wrapper tests, and assert on the real rules.list() return value. - Add an explicit enriched JSDoc on the public `fetch` field. - Fix the adjacent class-level example to use `fetch:` instead of the `fetcher:` callback the constructor deletes. --- .../unit/management-client-fetch.test.ts | 44 ++++++++++++++++++ src/management/wrapper/ManagementClient.ts | 42 +++++++++++------ tests/management/ManagementClient.test.ts | 46 ------------------- 3 files changed, 73 insertions(+), 59 deletions(-) create mode 100644 src/management/tests/unit/management-client-fetch.test.ts delete mode 100644 tests/management/ManagementClient.test.ts diff --git a/src/management/tests/unit/management-client-fetch.test.ts b/src/management/tests/unit/management-client-fetch.test.ts new file mode 100644 index 0000000000..400a6f4a69 --- /dev/null +++ b/src/management/tests/unit/management-client-fetch.test.ts @@ -0,0 +1,44 @@ +import { ManagementClient } from "../../wrapper/ManagementClient.js"; + +describe("ManagementClient custom fetch option", () => { + // Regression for https://github.com/auth0/node-auth0/issues/1330 + // The wrapper used to `delete (_options as any).fetch`, which silently + // dropped any caller-provided custom fetch implementation even though the + // underlying `fetcherImpl` honours `fetchFn`. These tests verify the + // wrapper now forwards `fetch` all the way through to the transport. + it("should route requests through the provided custom fetch implementation", async () => { + const calls: { url: string; init?: RequestInit }[] = []; + const customFetch: typeof fetch = (async (input, init) => { + const url = typeof input === "string" ? input : (input as URL | Request).toString(); + calls.push({ url, init }); + return new Response(JSON.stringify({ rules: [{ id: "rul_test", name: "Test Rule", enabled: true }] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof fetch; + + const client = new ManagementClient({ + domain: "tenant.auth0.com", + token: "test-token", + fetch: customFetch, + }); + + const page = await client.rules.list(); + + // The custom fetch was actually used for the request... + expect(calls.length).toBeGreaterThanOrEqual(1); + expect(calls[0].url).toContain("tenant.auth0.com/api/v2/rules"); + // ...and the response it returned is what the client parsed and handed back. + expect(page.data).toEqual([{ id: "rul_test", name: "Test Rule", enabled: true }]); + }); + + it("should not throw when the `fetch` option is omitted (defaults to global fetch)", () => { + expect( + () => + new ManagementClient({ + domain: "tenant.auth0.com", + token: "test-token", + }), + ).not.toThrow(); + }); +}); diff --git a/src/management/wrapper/ManagementClient.ts b/src/management/wrapper/ManagementClient.ts index 5d852f2b73..58b59e8d1b 100644 --- a/src/management/wrapper/ManagementClient.ts +++ b/src/management/wrapper/ManagementClient.ts @@ -22,12 +22,29 @@ export declare namespace ManagementClient { * @group Management API * @public */ - export interface ManagementClientOptions extends Omit< - FernClient.Options, - "token" | "environment" | "fetcher" | "baseUrl" - > { + export interface ManagementClientOptions + extends Omit { /** Auth0 domain (e.g., 'your-tenant.auth0.com') */ domain: string; + /** + * Custom `fetch` implementation used for all HTTP requests. + * + * Provide your own transport when you need to control how requests are + * made — for example to route through a proxy or corporate egress, add + * retry middleware, attach OpenTelemetry instrumentation, or run on + * platforms where the global `fetch` must be swapped (Cloudflare + * Workers, Bun). Defaults to the platform's global `fetch`. + * + * @example + * ```typescript + * const client = new ManagementClient({ + * domain: 'your-tenant.auth0.com', + * token: 'your-static-token', + * fetch: myInstrumentedFetch, + * }); + * ``` + */ + fetch?: typeof fetch; /** * API audience. Defaults to https://{domain}/api/v2/ * @defaultValue `https://{domain}/api/v2/` @@ -182,16 +199,16 @@ export declare namespace ManagementClient { * }); * ``` * - * @example Using custom fetcher with custom domain header (they work together) + * @example Using a custom fetch with custom domain header (they work together) * ```typescript * const client = new ManagementClient({ * domain: 'your-tenant.auth0.com', * clientId: 'your-client-id', * clientSecret: 'your-client-secret', * withCustomDomainHeader: 'auth.example.com', // Custom domain header logic - * fetcher: async (args) => { - * console.log('Making request:', args.url); // Custom logging - * return fetch(args.url, { ...args }); // Custom fetch implementation + * fetch: (input, init) => { + * console.log('Making request:', input); // Custom logging + * return fetch(input, init); // Custom fetch implementation * } * }); * ``` @@ -208,11 +225,10 @@ export class ManagementClient extends FernClient { const headers = createTelemetryHeaders(_options); const token = createTokenSupplier(_options); - // The underlying fetcher type is internal to the generated Fern client - // and is intentionally kept off the public surface. `fetch`, however, - // is supported all the way down through `fetcherImpl` and is a - // legitimate customization hook for callers that need to control the - // HTTP transport (proxies, retries, instrumentation). + // The underlying `fetcher` type is a Fern implementation detail and is + // intentionally kept off the public surface, so strip it here. `fetch`, + // by contrast, is supported all the way down through `fetcherImpl` and + // is a legitimate transport-customization hook, so it is left in place. // https://github.com/auth0/node-auth0/issues/1330 delete (_options as any).fetcher; diff --git a/tests/management/ManagementClient.test.ts b/tests/management/ManagementClient.test.ts deleted file mode 100644 index 3df660d74d..0000000000 --- a/tests/management/ManagementClient.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ManagementClient } from "../../src/management/index.js"; - -describe("ManagementClient (wrapper)", () => { - describe("custom fetch option", () => { - // Regression for https://github.com/auth0/node-auth0/issues/1330 - // The wrapper used to `delete (_options as any).fetch`, which silently - // dropped any caller-provided custom fetch implementation even though - // the underlying `fetcherImpl` honours `fetchFn`. Verify the wrapper - // now forwards `fetch` all the way through. - it("should invoke the provided custom fetch implementation", async () => { - const calls: { url: string; init?: RequestInit }[] = []; - const customFetch: typeof fetch = (async (input, init) => { - const url = typeof input === "string" ? input : (input as URL | Request).toString(); - calls.push({ url, init }); - return new Response(JSON.stringify([{ id: "rul_test", name: "Test Rule" }]), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }) as typeof fetch; - - const client = new ManagementClient({ - domain: "tenant.auth0.com", - token: "test-token", - fetch: customFetch, - }); - - await client.rules.list().catch(() => { - // Iterator parsing may not match the mocked payload shape; the - // assertion below only cares that the custom fetch was invoked. - }); - - expect(calls.length).toBeGreaterThanOrEqual(1); - expect(calls[0].url).toContain("tenant.auth0.com/api/v2/rules"); - }); - - it("should not throw when the `fetch` option is omitted (defaults to global fetch)", () => { - expect( - () => - new ManagementClient({ - domain: "tenant.auth0.com", - token: "test-token", - }), - ).not.toThrow(); - }); - }); -});