Skip to content
Open
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
3 changes: 3 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Specify files that shouldn't be modified by Fern
src/wrapper/
src/index.ts
tests/custom.test.ts
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * as AgentMail from "./api/index.js";
export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js";
export { AgentMailClient } from "./Client.js";
export { AgentMailClient } from "./wrapper/AgentMailClient.js";
export { AgentMailEnvironment, type AgentMailEnvironmentUrls } from "./environments.js";
export { AgentMailError, AgentMailTimeoutError } from "./errors/index.js";
export * from "./exports.js";
Expand Down
66 changes: 66 additions & 0 deletions src/wrapper/AgentMailClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { AgentMailClient as FernAgentMailClient } from "../Client.js";
import type { BaseClientOptions, BaseRequestOptions } from "../BaseClient.js";
import { WebsocketsClient } from "../api/resources/websockets/client/Client.js";
import { X402WebsocketsClient } from "./X402WebsocketsClient.js";

export declare namespace AgentMailClient {
export interface Options extends BaseClientOptions {
/**
* An x402Client instance for automatic payment handling on HTTP and WebSocket calls.
*
* @example
* ```typescript
* import { x402Client } from "@x402/fetch";
* import { registerExactEvmScheme } from "@x402/evm/exact/client";
* import { privateKeyToAccount } from "viem/accounts";
*
* const x402 = new x402Client();
* registerExactEvmScheme(x402, { signer: privateKeyToAccount("0x...") });
*
* const client = new AgentMailClient({
* environment: AgentMailEnvironment.ProdX402,
* x402,
* });
* ```
*/
x402?: unknown;
}

export type RequestOptions = BaseRequestOptions;
}

export class AgentMailClient extends FernAgentMailClient {
private readonly _x402Client?: unknown;

constructor(options: AgentMailClient.Options = {}) {
if (options.x402) {
let wrappedFetch: typeof fetch | undefined;
const x402Client = options.x402;

super({
apiKey: "",
...options,
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
if (!wrappedFetch) {
const { wrapFetchWithPayment } = await import("@x402/fetch");
wrappedFetch = wrapFetchWithPayment(fetch, x402Client as never);
}
return wrappedFetch(input, init);
}) as typeof fetch,
});

this._x402Client = x402Client;
} else {
super(options);
}
}

public get websockets(): WebsocketsClient {
if (!this._websockets) {
this._websockets = this._x402Client
? new X402WebsocketsClient(this._options, this._x402Client)
: new WebsocketsClient(this._options);
}
return this._websockets;
}
}
30 changes: 30 additions & 0 deletions src/wrapper/X402WebsocketsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { WebsocketsClient } from "../api/resources/websockets/client/Client.js";
import type { WebsocketsSocket } from "../api/resources/websockets/client/Socket.js";
import * as core from "../core/index.js";
import * as environments from "../environments.js";
import { getPaymentHeaders } from "./x402.js";

export class X402WebsocketsClient extends WebsocketsClient {
private readonly _x402Client: unknown;

constructor(options: WebsocketsClient.Options, x402Client: unknown) {
super(options);
this._x402Client = x402Client;
}

public async connect(args: WebsocketsClient.ConnectArgs = {}): Promise<WebsocketsSocket> {
const wsUrl = core.url.join(
(await core.Supplier.get(this._options.baseUrl)) ??
((await core.Supplier.get(this._options.environment)) ?? environments.AgentMailEnvironment.Prod)
.websockets,
"/v0",
);

const paymentHeaders = await getPaymentHeaders(wsUrl, this._x402Client);

return super.connect({
...args,
headers: { ...paymentHeaders, ...args.headers },
});
}
}
1 change: 1 addition & 0 deletions src/wrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AgentMailClient } from "./AgentMailClient.js";
13 changes: 13 additions & 0 deletions src/wrapper/x402-modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Type declarations for @x402/fetch (optional peer dependency, dynamically imported)

declare module "@x402/fetch" {
export function wrapFetchWithPayment(
fetch: typeof globalThis.fetch,
client: unknown,
): typeof globalThis.fetch;
export class x402HTTPClient {
constructor(client: unknown);
getPaymentRequiredResponse(getHeader: (name: string) => string | null, body: unknown): unknown;
encodePaymentSignatureHeader(payload: unknown): Record<string, string>;
}
}
37 changes: 37 additions & 0 deletions src/wrapper/x402.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Probes a WebSocket endpoint over HTTP to get a 402 response,
* then signs a payment and returns the headers needed for the WS handshake.
*/
export async function getPaymentHeaders(wsUrl: string, x402Client: unknown): Promise<Record<string, string>> {
let x402Fetch: typeof import("@x402/fetch");
try {
x402Fetch = await import("@x402/fetch");
} catch {
throw new Error(
'x402 WebSocket support requires @x402/fetch to be installed. Run: npm install @x402/fetch',
);
}

const httpClient = new x402Fetch.x402HTTPClient(x402Client as never);
const payClient = x402Client as { createPaymentPayload(req: unknown): Promise<unknown> };

const httpUrl = wsUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");

const response = await fetch(httpUrl);
if (response.status !== 402) {
const body = await response.text();
throw new Error(`x402: expected 402 from ${httpUrl} but got ${response.status}: ${body || "(empty)"}`);
}

let body: unknown;
try {
body = JSON.parse(await response.text());
} catch {
body = undefined;
}

const getHeader = (name: string): string | null => response.headers.get(name);
const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body);
const paymentPayload = await payClient.createPaymentPayload(paymentRequired);
return httpClient.encodePaymentSignatureHeader(paymentPayload);
}