Skip to content
Merged
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
21 changes: 10 additions & 11 deletions src/wrapper/Client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { x402Client } from "@x402/fetch";
import type { Mppx } from "mppx/client";

import { Supplier } from "../core/index.js";
import { NoOpAuthProvider } from "../core/auth/NoOpAuthProvider.js";
Expand All @@ -8,16 +7,16 @@ import { AgentMailClient as FernAgentMailClient } from "../Client.js";
import { type GetPaymentCredentials, WebsocketsClient } from "./WebsocketsClient.js";

import { getPaymentCredentials as getX402Credentials } from "./x402.js";
import { getPaymentCredentials as getMppCredentials } from "./mpp.js";
import { type MppxClient, getPaymentCredentials as getMppCredentials } from "./mppx.js";

type SharedOptions = Omit<FernAgentMailClient.Options, "apiKey">;

export declare namespace AgentMailClient {
export type Options = SharedOptions &
(
| { x402: x402Client; mpp?: never; apiKey?: never }
| { mpp: Mppx.Mppx; x402?: never; apiKey?: never }
| { apiKey?: Supplier<string>; x402?: never; mpp?: never }
| { x402: x402Client; mppx?: never; apiKey?: never }
| { mppx: MppxClient; x402?: never; apiKey?: never }
| { apiKey?: Supplier<string>; x402?: never; mppx?: never }
);
export type RequestOptions = FernAgentMailClient.RequestOptions;
}
Expand All @@ -40,8 +39,8 @@ export class AgentMailClient extends FernAgentMailClient {
authProvider: new NoOpAuthProvider(),
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
if (!wrappedFetch) {
const mod = await import("@x402/fetch");
wrappedFetch = mod.wrapFetchWithPayment(fetch, x402);
const { wrapFetchWithPayment } = await import("@x402/fetch");
wrappedFetch = wrapFetchWithPayment(fetch, x402);
}
return wrappedFetch(input, init);
},
Expand All @@ -54,13 +53,13 @@ export class AgentMailClient extends FernAgentMailClient {
super(fernOptions);

this._getPaymentCredentials = (wsUrl) => getX402Credentials(wsUrl, x402);
} else if (options.mpp) {
const { mpp, ...rest } = options;
} else if (options.mppx) {
const { mppx, ...rest } = options;

const fernOptions = {
...rest,
authProvider: new NoOpAuthProvider(),
fetch: mpp.fetch,
fetch: mppx.fetch,
};

if (!fernOptions.environment && !fernOptions.baseUrl) {
Expand All @@ -69,7 +68,7 @@ export class AgentMailClient extends FernAgentMailClient {

super(fernOptions);

this._getPaymentCredentials = (wsUrl) => getMppCredentials(wsUrl, mpp);
this._getPaymentCredentials = (wsUrl) => getMppCredentials(wsUrl, mppx);
} else {
let fernOptions: FernAgentMailClient.Options = options;

Expand Down
11 changes: 0 additions & 11 deletions src/wrapper/mpp-types.d.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/wrapper/mpp.ts

This file was deleted.

22 changes: 22 additions & 0 deletions src/wrapper/mppx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { probe402, wsToHttp } from "./util.js";

export interface MppxClient {
fetch: typeof globalThis.fetch;
transport: {
setCredential(request: Request, credential: string): Request;
};
createCredential(response: Response): Promise<string>;
}

export async function getPaymentCredentials(wsUrl: string, mppx: MppxClient): Promise<Record<string, string>> {
const response = await probe402(wsUrl);

const credential = await mppx.createCredential(response);
const signed = mppx.transport.setCredential(new Request(wsToHttp(wsUrl)), credential);

const headers: Record<string, string> = {};
signed.headers.forEach((value: string, key: string) => {
headers[key] = value;
});
return headers;
}
7 changes: 6 additions & 1 deletion src/wrapper/probe402.ts → src/wrapper/util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
export function wsToHttp(wsUrl: string): string {
return wsUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
}

export async function probe402(wsUrl: string): Promise<Response> {
const httpUrl = wsUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
const httpUrl = wsToHttp(wsUrl);

const response = await fetch(httpUrl);
if (response.status !== 402) {
throw new Error(`Expected 402 from ${httpUrl} but got ${response.status}`);
}

return response;
}
2 changes: 1 addition & 1 deletion src/wrapper/x402.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { x402Client } from "@x402/fetch";
import { probe402 } from "./probe402.js";
import { probe402 } from "./util.js";

export async function getPaymentCredentials(wsUrl: string, client: x402Client): Promise<Record<string, string>> {
const { x402HTTPClient } = await import("@x402/fetch");
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/wrapper/Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe("AgentMailClient environment selection", () => {
});
});

describe("with mpp", () => {
describe("with mppx", () => {
const mockMppFetch = vi.fn().mockResolvedValue(new Response());
const mockMppClient = {
fetch: mockMppFetch,
Expand All @@ -111,22 +111,22 @@ describe("AgentMailClient environment selection", () => {
};

it("should derive ProdMpp environment", async () => {
const client = new AgentMailClient({ mpp: mockMppClient });
const client = new AgentMailClient({ mppx: mockMppClient });
const env = await Supplier.get(client["_options"].environment);
expect(env).toEqual(AgentMailEnvironment.ProdMpp);
});

it("should not override explicitly set environment", async () => {
const client = new AgentMailClient({
mpp: mockMppClient,
mppx: mockMppClient,
environment: AgentMailEnvironment.EuProd,
});
const env = await Supplier.get(client["_options"].environment);
expect(env).toEqual(AgentMailEnvironment.EuProd);
});

it("should use mpp.fetch as the fetch implementation", () => {
const client = new AgentMailClient({ mpp: mockMppClient });
it("should use mppx.fetch as the fetch implementation", () => {
const client = new AgentMailClient({ mppx: mockMppClient });
expect(client["_options"].fetch).toBe(mockMppFetch);
});
});
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/wrapper/WebsocketsClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AgentMailClient } from "../../../src/wrapper/Client";
import { WebsocketsClient as FernWebsocketsClient } from "../../../src/api/resources/websockets/client/Client";
import type { WebsocketsSocket } from "../../../src/api/resources/websockets/client/Socket";
import * as x402Helpers from "../../../src/wrapper/x402";
import * as mppHelpers from "../../../src/wrapper/mpp";
import * as mppHelpers from "../../../src/wrapper/mppx";

function mockConnect() {
return vi.spyOn(FernWebsocketsClient.prototype, "connect").mockResolvedValue({} as WebsocketsSocket);
Expand Down Expand Up @@ -94,7 +94,7 @@ describe("WebsocketsClient wrapper", () => {
});
});

describe("with mpp", () => {
describe("with mppx", () => {
const mockMppClient = {
fetch: vi.fn(),
transport: { setCredential: vi.fn() },
Expand All @@ -105,7 +105,7 @@ describe("WebsocketsClient wrapper", () => {
it("should call getPaymentCredentials and pass as queryParams", async () => {
const spy = vi.spyOn(mppHelpers, "getPaymentCredentials").mockResolvedValue(mockCredentials);

const client = new AgentMailClient({ mpp: mockMppClient });
const client = new AgentMailClient({ mppx: mockMppClient });
await client.websockets.connect();

expect(spy).toHaveBeenCalled();
Expand All @@ -121,7 +121,7 @@ describe("WebsocketsClient wrapper", () => {
it("should let user queryParams override payment credentials", async () => {
const spy = vi.spyOn(mppHelpers, "getPaymentCredentials").mockResolvedValue({ Authorization: "from-mpp" });

const client = new AgentMailClient({ mpp: mockMppClient });
const client = new AgentMailClient({ mppx: mockMppClient });
await client.websockets.connect({ queryParams: { Authorization: "user-override" } });

expect(connectSpy).toHaveBeenCalledWith(
Expand Down