rspace-online/shared/x402/crdt-client.ts

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,
},
});
};
}