/** * taint-gate.ts, who may INITIATE a purchase, and who may choose the merchant. * * These are two different questions and the design rule treats them * differently. This module used to conflate them, refusing any purchase whose * merchant came from page content and documenting that as a feature. Design * rule that overrides it: the taint gate was too strict, because asking the * agent to buy the cheapest match of something found online should let it * find the item, show it to the owner, and then alert the owner before * purchasing when the merchant is not a major retailer, using its own * judgement of what counts as major. * * ══ What relaxed, and what did not ════════════════════════════════════════ * * RELAXED, **who chooses the merchant** on a purchase the owner initiated. * "Buy the cheapest X you can find" is the owner's instruction; the item and * the intent are theirs, and only the storefront was found on a page. That now proceeds, with the * merchant graded by `merchant-recourse.ts` into a veto (silence proceeds) or an * approval (silence denies). * * NOT RELAXED, **who initiates.** Content-initiated purchases are refused * absolutely. An email or a web page saying "buy X from Y" cannot start a * purchase, cannot name a merchant, and cannot set an amount. There is no * owner-approval escape hatch, for the same reason this module has always argued * for money: the approval is exactly the step an injection is trying to reach. * * The distinction is carried in the TYPE rather than checked at runtime. * `merchantDiscovered` exists only on `OwnerOriginIntent`, so "a discovered * merchant is permissible only on an owner-origin intent" is a fact the compiler * enforces rather than a rule a later edit can forget. * * ══ Why this does not call evaluateOutwardEffect ══════════════════════════ * * `security/untrusted-content.ts` exposes `evaluateOutwardEffect`, which * `email.send` uses. It accepts an `OwnerApproval` and, when one matches the * action, returns `allowed: true` FOR TAINTED CONTENT. That escape hatch is * right for email, the owner can decide to forward something a stranger wrote, * and wrong for money. The reliable way to guarantee it cannot fire is to not be * on that code path, so this module calls `findContentTaint` directly. A test * passes a valid `OwnerApproval` for the same action and asserts a * content-initiated purchase is still refused. * * ══ Which fields are checked ══════════════════════════════════════════════ * * ALWAYS, `item` and `requestedMax`. These come from the owner or the purchase * does not exist. A page that supplies the thing to buy, or the ceiling to buy it * under, is initiating a purchase whatever else is true. * * CONDITIONALLY, `merchant` and `checkoutUrl`. Checked when the owner NAMED * the merchant, because then it has to be theirs. Not checked when * `merchantDiscovered` is set, because there the storefront came off a page by * design, and grading it, not refusing it, is the safeguard. * * NEVER, the merchant's quoted price, tax, fees and shipping. They are read * from the merchant by definition; checking them would refuse every purchase and * the check would be removed within a release. The BUDGET is their defence: an * inflated price hits the daily budget or the per-purchase ceiling and needs an * approval, showing our own re-rendered number. * * See docs/decisions/2026-07-27-a-discovered-merchant-is-graded-not-refused.md * for the full record of the override, and docs/payments.md §9.1. */ import { type TaintFinding } from '../security/content-taint.js'; import type { UntrustedContentLedger } from '../security/untrusted-content.js'; /** * A purchase the OWNER asked for. * * `merchantDiscovered` says whether the storefront was found while browsing * rather than named by the owner. It lives only on this variant, a content-origin * intent never reaches the point of choosing a merchant. */ export interface OwnerOriginIntent { readonly origin: 'owner'; readonly merchantDiscovered: boolean; readonly merchant: string | undefined; readonly checkoutUrl: string | undefined; readonly item: string | undefined; readonly requestedMax: string | undefined; } /** * A purchase something else asked for, a page, an email, a channel message. * * Deliberately has no `merchantDiscovered` field. Every intent of this shape is * refused, so it never gets to choose anything. */ export interface ContentOriginIntent { readonly origin: 'content'; readonly merchant: string | undefined; readonly checkoutUrl: string | undefined; readonly item: string | undefined; readonly requestedMax: string | undefined; } export type PaymentIntent = OwnerOriginIntent | ContentOriginIntent; export interface PaymentTaintDecision { readonly allowed: boolean; readonly findings: readonly TaintFinding[]; readonly reason: string | null; /** Which fields were examined, so a decision can be reconstructed later. */ readonly checkedFields: readonly string[]; } /** * The refusal for a purchase the owner did not initiate. * * Terminal. No approval, no downgrade, no notification-based rescue. */ export declare function describeContentInitiatedRefusal(): string; /** * Evaluate a payment intent against the untrusted content read this turn. * * `ledger` is the process-wide untrusted-content ledger, the same one the * browser's page reads and the mail surface's body reads both record into, so * "read a stranger's page, then buy something" is visible here as one act. */ export declare function evaluatePaymentTaint(input: { readonly intent: PaymentIntent; readonly ledger: UntrustedContentLedger; }): PaymentTaintDecision; /** * The refusal the owner reads. * * Names the field, the surface and the origin and shows the overlapping text, * because "refused: untrusted content" with no evidence is indistinguishable * from a bug and gets worked around. */ export declare function describePaymentTaint(findings: readonly TaintFinding[]): string; //# sourceMappingURL=taint-gate.d.ts.map