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 ba96160d73..58b59e8d1b 100644 --- a/src/management/wrapper/ManagementClient.ts +++ b/src/management/wrapper/ManagementClient.ts @@ -23,9 +23,28 @@ export declare namespace ManagementClient { * @public */ export interface ManagementClientOptions - extends Omit { + 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/` @@ -180,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 * } * }); * ``` @@ -206,9 +225,12 @@ 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 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; - delete (_options as any).fetch; // Prepare the base client options let clientOptions: any = {