/** * routes/payments.ts, the daemon actually serving `payments.*`. * * The daemon is the process that holds the card and charges it, with every * surface closed and across restarts, so these handlers are the only way a * surface sees or changes any of it. This module is deliberately thin: it maps * the descriptors' declared shapes onto a narrow service slice, performs no * I/O, holds no credential, and never touches a card number. * * Two properties are enforced HERE rather than merely advertised, so neither * rests on schema validation being reached by every transport: * * - **Card material is write-only.** `cards.create` takes the number, expiry, * CVV and cardholder name and returns the METADATA record. Nothing in this * module can return a stored secret, and `sanitizeCardMetadata` strips any * field a future service might mistakenly hand back, a service bug becomes * a missing field rather than a leaked card. * - **Card material never reaches an error.** Failures report the stage and a * plain reason; what was submitted is never part of a diagnostic, because * an error string is a read path like any other. * * ── `payments.checkout.fillCard` and why the header above still holds ───── * * The capability has to be able to type the card into a checkout or it cannot * buy anything, and the original write-only wording forbade exactly that. The * correction keeps every property this module actually enforces: * * - This module still performs no I/O, holds no credential, and never touches * a card number. `fillCard`'s handler reads a session, a page, a card id and * a list of field targets, hands them to the service, and returns the * service's field names and boolean. * - Nothing is echoed. The output has no property that could hold a value, * and `sanitizeFillResult` rebuilds the response from an allowlist for the * same reason `sanitizeCardMetadata` does, a service bug becomes a missing * field rather than a leaked card. * - A failure names the FIELD, never the value. The service's own error is * discarded rather than forwarded. * * What changed is only who does the typing: the DAEMON reads the material in * its own process and puts it in the field. The model orchestrates the purchase * and never holds the instrument, which was the property worth having. */ import type { GatewayMethodCatalog } from '../method-catalog.js'; import type { GatewayMethodHandler } from '../method-catalog-shared.js'; import type { CardMetadata } from '../../payments/types.js'; import type { PoolSnapshot } from '../../payments/budget.js'; /** What a purchase looks like once the audit ledger has recorded it. */ export interface PaymentPurchaseView { readonly purchaseId: string; readonly atUtc: string; readonly dayKey: string; readonly timezone: string; readonly merchantDomain: string; readonly item: string; readonly currency: string; readonly itemMinorUnits: number; readonly taxMinorUnits: number; readonly feesMinorUnits: number; readonly shippingMinorUnits: number; readonly totalMinorUnits: number; readonly shippingTierRequested: string; readonly shippingTierUsed: string; readonly steppedDown: boolean; readonly itemPoolDraw: number; readonly overagePoolDraw: number; readonly tolerancePoolDraw: number; readonly cardLast4: string; readonly windowKind: string; readonly windowOutcome: string; readonly answeredBy: string | null; readonly outcome: string; readonly refusalReason: string | null; readonly merchantOrderId: string | null; readonly refundedAt: string | null; /** Whether the merchant carried established recourse, and on what grounds. */ readonly merchantRecognised: boolean; readonly merchantQualifier: string | null; /** Whether the owner named the storefront or it was found while browsing. */ readonly merchantDiscovered: boolean; } export interface PaymentsBudgetView { readonly enabled: boolean; readonly currency: string; readonly pools: PoolSnapshot; readonly reservationCount: number; readonly isPaymentsLeader: boolean; } /** Card metadata plus whether the secret store holds every required field. */ export interface PaymentCardView extends CardMetadata { readonly materialComplete: boolean; } /** The narrow slice of the capability these handlers need. */ export interface PaymentsGatewayService { /** * Settle checkouts a restart interrupted, per the journal's phase verdicts * and the windows' documented silence rules, returning the sweep envelope * (`CheckoutRecoverySweep`: swept or skipped, plus settlements). Optional * so a narrower test double stays valid; when present, registration runs * it to completion at attach time, before any checkout verb can be served. */ recoverInterruptedCheckouts?(): Promise; budgetStatus(): Promise; listCards(): Promise<{ cards: readonly PaymentCardView[]; defaultCardId: string; }>; /** * Store a card. Implementations write the material to the daemon secret * store and MUST NOT return any of it. */ createCard(input: { readonly label: string; readonly kind: 'virtual' | 'real'; readonly number: string; readonly expiryMonth: number; readonly expiryYear: number; readonly cvv: string; readonly cardholderName: string; readonly issuerCapMinorUnits: number | null; }): Promise; deleteCard(id: string): Promise<{ deleted: boolean; secretsCleared: number; }>; /** * Type the stored card into an open checkout page. * * The implementation reads the material from the daemon secret store, checks * that a purchase is in flight on that page and that the page is still on the * merchant it was decided against, and types. It MUST NOT return any part of * the material, and MUST NOT include any of it in a thrown error. */ /** * Run a purchase against an open browser page. * * The implementation owns the whole decision order; this module only shapes * the call. Amounts arrive as STRINGS and are parsed by the daemon, so no * number on this path was parsed by a caller. */ beginCheckout(input: PaymentBeginCheckoutInput): Promise; fillCardIntoCheckout(input: { readonly sessionId: string; readonly pageId: string; readonly targets: readonly { readonly field: string; readonly ref: string; }[]; readonly expirySeparator: string | undefined; readonly twoDigitYear: boolean | undefined; }): Promise; listPurchases(input: { readonly limit: number; readonly dayKey: string | undefined; }): Promise<{ purchases: readonly PaymentPurchaseView[]; total: number; }>; } /** The shape `payments.checkout.begin` hands the service, already validated. */ export interface PaymentBeginCheckoutInput { readonly sessionId: string; readonly pageId: string; readonly merchantDomain: string; readonly checkoutUrl: string; readonly item: string; readonly cardId: string; readonly requestedLines: readonly { readonly label: string; readonly quantity: number; }[]; readonly reading: { readonly lines: readonly { readonly label: string; readonly quantity: string; readonly unitPrice: string; }[]; readonly tax: string | null; readonly fees: readonly { readonly label: string; readonly amount: string; }[]; readonly shippingOptions: readonly { readonly label: string; readonly cost: string; }[]; readonly statedTotal: string | null; readonly currency: string | null; readonly orderSummaryText: string; }; readonly controls: { readonly cardFields: readonly { readonly field: string; readonly ref: string; }[]; readonly addressFields: readonly { readonly kind: string; readonly field: string; readonly ref: string; }[]; readonly shippingTargets: readonly string[]; readonly placeOrderTarget: string; readonly expirySeparator: string | undefined; readonly twoDigitYear: boolean | undefined; }; readonly preferredTier: string | undefined; readonly requestedMax: string | undefined; } /** What a begin reports. Amounts are integers the daemon computed. */ export interface PaymentBeginResultView { readonly outcome: string; readonly purchaseId: string | null; readonly reason: string | null; readonly merchantOrderId: string | null; readonly totalMinorUnits: number | null; readonly currency: string | null; readonly shippingTierUsed: string | null; readonly steppedDown: boolean; readonly challengeStep: string | null; } /** What a fill reports. Field names and a boolean, nothing that holds a value. */ export interface PaymentFillCardResult { readonly ok: boolean; readonly filled: readonly string[]; readonly failedField: string | null; readonly reason: string | null; } export declare function createPaymentsBudgetStatusHandler(service: PaymentsGatewayService): GatewayMethodHandler; export declare function createPaymentsCardsListHandler(service: PaymentsGatewayService): GatewayMethodHandler; export declare function createPaymentsCardsCreateHandler(service: PaymentsGatewayService): GatewayMethodHandler; export declare function createPaymentsCardsDeleteHandler(service: PaymentsGatewayService): GatewayMethodHandler; /** * `payments.checkout.begin`. * * Validates shape and nothing else. Every judgement about MEANING, whether an * amount parses, whether the cart matches, whether the budget covers it, is * the service's, because those are the decisions that must not be reachable by * a caller that skipped this route. */ export declare function createPaymentsCheckoutBeginHandler(service: PaymentsGatewayService): GatewayMethodHandler; export declare function createPaymentsCheckoutFillCardHandler(service: PaymentsGatewayService): GatewayMethodHandler; export declare function createPaymentsPurchasesListHandler(service: PaymentsGatewayService): GatewayMethodHandler; /** * Attach the payment handlers to their registered descriptors (missing = no-op). * * Boot recovery runs TO COMPLETION before any handler attaches, so no checkout * verb can start a purchase the sweep would then read as interrupted. The * promise resolves once the verbs are attached; a caller that does not await * it serves the payment verbs a beat later, never a swept-mid-flight checkout. * Recovery failure, thrown synchronously or rejected, is reported through * `onRecoveryFailure` and does not withhold the verbs: the sweep is a * disclosure duty, not a serving precondition once it has stopped running. */ export declare function registerPaymentsGatewayMethods(catalog: GatewayMethodCatalog, service: PaymentsGatewayService, options?: { /** Reported when boot recovery itself fails; never carries a notice body. */ readonly onRecoveryFailure?: ((error: unknown) => void) | undefined; /** * Receives the sweep envelope every time recovery runs, including a * skipped-not-leader boot, for the composition's audit log; disclosure * of recoveries is the platform rule, and the envelope is the audit * record. */ readonly onRecoverySettled?: ((sweep: unknown) => void) | undefined; }): Promise; //# sourceMappingURL=payments.d.ts.map