Files
WRNexusJS/packages/payment/src/http.ts
T

97 lines
3.1 KiB
TypeScript

import type { InitializePaymentInput, PaymentService } from "./core.ts";
import type { PaymentIntent } from "./types.ts";
export type PaymentApiAction =
| "list"
| "read"
| "initialize"
| "sync"
| "cancel"
| "capture"
| "refund"
| "methods"
| "reconcile";
export interface PaymentApiContext {
req: Request;
params?: Record<string, string>;
user?: { id?: string };
session?: { userId?: string; subject?: string };
}
export interface PaymentApiConfig {
enabled?: boolean;
/** Resolve the authenticated subject on the server. Request-body subject values are ignored. */
subject?(context: PaymentApiContext): string | undefined | Promise<string | undefined>;
/** Additional authorization. Administrative actions deny by default. */
authorize?(
context: PaymentApiContext,
action: PaymentApiAction,
payment?: PaymentIntent,
): boolean | Promise<boolean>;
/** Replace client input with authoritative catalog/order data before creating an intent. */
initialize?(
context: PaymentApiContext,
body: Record<string, unknown>,
subject: string,
): Omit<InitializePaymentInput, "subject"> | Promise<Omit<InitializePaymentInput, "subject">>;
}
const HTTP = Symbol.for("@wrnexus/payment:http:v1");
type PaymentHttpGlobal = typeof globalThis & { [HTTP]?: PaymentApiConfig };
export function configurePaymentApi(config: PaymentApiConfig = {}) {
(globalThis as PaymentHttpGlobal)[HTTP] = config;
}
export function getPaymentApiConfig(): PaymentApiConfig {
return (globalThis as PaymentHttpGlobal)[HTTP] ?? {};
}
export async function paymentApiSubject(context: PaymentApiContext) {
const config = getPaymentApiConfig();
const resolved = await config.subject?.(context);
return resolved?.trim() || context.user?.id?.trim() || context.session?.userId?.trim() || "";
}
export async function paymentApiAllowed(
context: PaymentApiContext,
action: PaymentApiAction,
payment?: PaymentIntent,
) {
const config = getPaymentApiConfig();
if (config.enabled === false) return false;
if (config.authorize) return Boolean(await config.authorize(context, action, payment));
const subject = await paymentApiSubject(context);
if (!subject) return false;
if (["capture", "refund", "reconcile"].includes(action)) return false;
return !payment || payment.subject === subject;
}
export function publicPayment(payment: PaymentIntent, status: string) {
return {
id: payment.id,
amount: payment.amount,
currency: payment.currency,
gateway: payment.gateway,
status,
createdAt: payment.createdAt,
checkoutUrl: payment.checkoutUrl,
client: payment.client,
metadata: payment.metadata,
};
}
export async function ensureOwnedPayment(
service: PaymentService,
context: PaymentApiContext,
id: string,
action: PaymentApiAction,
) {
const payment = await service.getPayment(id);
if (!payment) return { response: Response.json({ error: "payment not found" }, { status: 404 }) };
if (!(await paymentApiAllowed(context, action, payment)))
return { response: Response.json({ error: "payment access denied" }, { status: 403 }) };
return { payment };
}