Skip to content
Draft
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
43 changes: 43 additions & 0 deletions src/management/tests/unit/wrapper/management-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ManagementClient } from "../../../wrapper/ManagementClient.js";

describe("ManagementClient with custom fetcher", () => {
const mockConfig = {
domain: "test-tenant.auth0.com",
token: "test-token",
};

beforeEach(() => {
jest.clearAllMocks();
});

it("should correctly pass the arguments", async () => {
const mockCustomFetcher = jest.fn().mockResolvedValue({
ok: true,
json: () => ({ actions: [{ id: "action_1" }] }),
headers: {},
});

const client = new ManagementClient({
...mockConfig,
fetcher: async (url, init, context) => {
return mockCustomFetcher(url, init, context);
},
});

const response = await client.actions.list();

expect(mockCustomFetcher).toHaveBeenNthCalledWith(
1,
"https://test-tenant.auth0.com/api/v2/actions/actions",
{
method: "GET",
headers: expect.objectContaining({ Authorization: "Bearer test-token" }),
},
// TODO: This should not be undefined once this is correctly implemented
{ scope: undefined },
);

expect(response.data.length).toBe(1);
expect(response.data[0].id).toBe("action_1");
});
});
65 changes: 61 additions & 4 deletions src/management/wrapper/ManagementClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { TokenProvider } from "./token-provider.js";
import { Auth0ClientTelemetry } from "../../lib/middleware/auth0-client-telemetry.js";
import { withCustomDomainHeader } from "../request-options.js";

export type FetchWithAuth = (
input: RequestInfo,
init?: RequestInit,
authParams?: { scope?: string },
) => Promise<Response>;

/**
* All supported configuration options for the ManagementClient.
*
Expand Down Expand Up @@ -43,6 +49,11 @@ export declare namespace ManagementClient {
* This works seamlessly with custom fetchers - both the custom domain logic and your custom fetcher will be applied.
*/
withCustomDomainHeader?: string;

/**
* Custom fetcher function to use for HTTP requests.
*/
fetcher?: FetchWithAuth;
}

/**
Expand Down Expand Up @@ -204,16 +215,15 @@ export class ManagementClient extends FernClient {
const baseUrl = `https://${_options.domain}/api/v2`;
const headers = createTelemetryHeaders(_options);
const token = createTokenSupplier(_options);

// Temporarily remove fetcher from options to avoid people passing it for now
delete (_options as any).fetcher;
const fetcher = createFetcher(_options.fetcher);

// Prepare the base client options
let clientOptions: any = {
let clientOptions: FernClient.Options = {
..._options,
baseUrl,
headers,
token,
fetcher,
};

// Apply custom domain header configuration if provided
Expand Down Expand Up @@ -328,3 +338,50 @@ function createTokenSupplier(_options: ManagementClientConfig): core.Supplier<st
return () => tokenProvider.getAccessToken();
}
}

/**
* Creates a fetcher function compatible with the Fern client.
* Wraps the provided fetch function to match the expected interface.
* @param fetcher - Custom fetch function
* @returns The fetcher function for the Fern client, or undefined if no fetcher is provided
*/
function createFetcher(fetcher?: FetchWithAuth): core.FetchFunction | undefined {
// When no fetcher is provided, return undefined to use the default fetcher.
if (!fetcher) {
return;
}

return async (args) => {
// This is future stuff that will be supported in FernClient's `core.Fetcher.Args`
const scope = (args as any).scope;

const response = await fetcher(
args.url,
{
method: args.method,
headers: args.headers as Record<string, string>,
body: args.body ? JSON.stringify(args.body) : undefined,
},
{ scope },
);

if (response.ok) {
return {
ok: true as const,
body: await response.json(),
headers: response.headers,
rawResponse: response,
};
} else {
return {
ok: false as const,
error: {
reason: "status-code" as const,
statusCode: response.status,
body: await response.text(),
},
rawResponse: response,
};
}
};
}
Loading