108 lines
2.8 KiB
TypeScript
108 lines
2.8 KiB
TypeScript
/**
|
|
* x402 CRDT payment — client-side helpers.
|
|
*
|
|
* Creates CRDT payment headers and wraps fetch to auto-handle
|
|
* 402 responses with CRDT scheme when available.
|
|
*/
|
|
|
|
export interface CrdtPaymentParams {
|
|
jwtToken: string;
|
|
fromDid: string;
|
|
fromLabel?: string;
|
|
amount: number;
|
|
tokenId: string;
|
|
}
|
|
|
|
/**
|
|
* Create a base64-encoded X-PAYMENT header for CRDT scheme.
|
|
*/
|
|
export function createCrdtPaymentHeader(params: CrdtPaymentParams): string {
|
|
const payload = {
|
|
scheme: "crdt" as const,
|
|
jwtToken: params.jwtToken,
|
|
fromDid: params.fromDid,
|
|
fromLabel: params.fromLabel,
|
|
amount: params.amount,
|
|
tokenId: params.tokenId,
|
|
nonce: crypto.randomUUID(),
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const json = JSON.stringify(payload);
|
|
return typeof btoa === "function"
|
|
? btoa(json)
|
|
: Buffer.from(json).toString("base64");
|
|
}
|
|
|
|
interface AuthProvider {
|
|
getToken: () => string | null;
|
|
getDid: () => string | null;
|
|
getLabel?: () => string | null;
|
|
getBalance?: (tokenId: string) => number;
|
|
}
|
|
|
|
/**
|
|
* Wrap fetch to auto-handle 402 responses with CRDT payment.
|
|
*
|
|
* On 402: checks if "crdt" scheme is offered and user has
|
|
* sufficient balance → auto-retries with CRDT payment header.
|
|
* Falls through if no CRDT option or insufficient balance.
|
|
*/
|
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
|
|
export function wrapFetchWithCrdtPayment(
|
|
baseFetch: FetchFn,
|
|
auth: AuthProvider,
|
|
): FetchFn {
|
|
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
const response = await baseFetch(input, init);
|
|
|
|
if (response.status !== 402) return response;
|
|
|
|
// Check if we have auth
|
|
const token = auth.getToken();
|
|
const did = auth.getDid();
|
|
if (!token || !did) return response;
|
|
|
|
// Parse 402 response for CRDT option
|
|
let body: any;
|
|
try {
|
|
body = await response.clone().json();
|
|
} catch {
|
|
return response;
|
|
}
|
|
|
|
const requirements = body.paymentRequirements;
|
|
if (!Array.isArray(requirements)) return response;
|
|
|
|
const crdtReq = requirements.find((r: any) => r.scheme === "crdt");
|
|
if (!crdtReq) return response;
|
|
|
|
const amount = parseInt(crdtReq.maxAmountRequired, 10);
|
|
const tokenId = crdtReq.tokenId;
|
|
|
|
// Check balance if provider available
|
|
if (auth.getBalance) {
|
|
const balance = auth.getBalance(tokenId);
|
|
if (balance < amount) return response; // Insufficient — return original 402
|
|
}
|
|
|
|
// Create CRDT payment header and retry
|
|
const paymentHeader = createCrdtPaymentHeader({
|
|
jwtToken: token,
|
|
fromDid: did,
|
|
fromLabel: auth.getLabel?.() || undefined,
|
|
amount,
|
|
tokenId,
|
|
});
|
|
|
|
return baseFetch(input, {
|
|
...init,
|
|
headers: {
|
|
...(init?.headers || {}),
|
|
"X-PAYMENT": paymentHeader,
|
|
},
|
|
});
|
|
};
|
|
}
|