Changelog

What changed, file by file.

This page is the repository's own CHANGELOG.md, read as each build starts, so the page and the file cannot disagree. Problems found and fixed are listed with the releases.

Unreleased

In development
  • Checkout theme: Settings now has a live preview of the hosted checkout and controls for the brand colour, page background, corner style (sharp, default, rounded), font (Inter, device, serif) and an uploaded logo, with a reset to pay's own look.
    • Derived, not typed: one brand colour yields the button, hover, selected and focus shades, link text and the text on the button, separately for light and dark. pay_core::theme holds every colour to WCAG 2.2 (button text 4.5:1, button and focus ring 3:1, link text 4.5:1, muted text and the card readable on a chosen background), moves a colour that falls short along its own hue as little as it takes, and tells the merchant what it changed. The saved colour is never rewritten. High contrast keeps pay's palette. The preview calls the same code as the payer pages' stylesheet (/m/{id}/brand.css, now versioned and cached), so the two cannot differ.
    • Logo upload: PNG, JPEG or WebP, 48 to 2048 pixels, up to 256 KB, identified by its own bytes (SVG is refused); stored in merchant_logos (migration 0007_checkout_theme, additive) and served from /m/{id}/logo with nosniff and a sandbox policy. The form is multipart, read by a small strict parser in pay-platform; no new dependency.
    • API (additive): branding on the merchant API takes and returns background, radius (sharp, default, round), font (inter, system, serif) and logo_uploaded beside color and logo_url; an empty string clears a field. The client checkout view gives an uploaded logo as a full address.
    • Dashboard form posts now accept a browser's Origin: null when Sec-Fetch-Site: same-origin says the post came from the page, because the pages are served with Referrer-Policy: no-referrer (fixes "Continue to payment" on Service credits and the back-office forms). Foreign and unmarked posts are still refused.
  • Rename the operator console to the back office. Its pages now live under /back-office (they were under /console); the session cookie is pay_back_office (__Host-pay_back_office over HTTPS), scoped to /back-office, so operators sign in again once; the configuration section is [back_office] and a leftover [console] section is refused at start-up; the sign-in audit action is back_office.signed_in on target back_office (rows written before keep console.signed_in); the Rust module is pay_platform::back_office and the templates are templates/back_office/. The old addresses answer a GET with a permanent redirect to the new ones (query string kept) in the app (308) and at the edge (301). Two things outside this repository follow from it: the edge now routes /back-office/ (see docs/ops/pen-test-scope.md), and the identity provider's registered post-logout address /console/sign-in should be changed to /back-office/sign-in (the redirect covers it until then). The sandbox's own console at / is a different tool and keeps its name.
  • Harden deployment verification: full-sync SQLite commits; staging/production refuse shared operator keys, replayed development back-office cookies and insecure public origins; application request logs use route templates and edge access logs redact payment capabilities and omit queries. Refresh the migration snapshot, test independent file-backed wallet connections, and replace the sandbox's vulnerable RSA dependency with ring and public simulator fixtures. Production launch still requires the controls and evidence in docs/reviews/production-readiness-2026-10-24.md.
  • Add the Provider Integration Protocol 1.0, the native door for payment service providers (D-014): a PSP implements pay's own provider API instead of ifinsta writing a connector for it.
    • Registry and lifecycle: PSPs and per-market listings (psp_, lst_), applied → under_review → approved_test → live → suspended → withdrawn from the contract's state machine, two-person approval of every step that opens or closes money, secrets per direction with a 7-day rotation overlap, health per method, suspension that keeps methods on payer pages marked unavailable.
    • Wire: Provider-Signature over method, target (query string included) and raw body, verified in constant time with two-value rotation; the PSP payment object read strictly (integer minor units, closed vocabularies); idempotent calls on the id pay sends; notifications deduped on (listing, id), applied only if the PSP's sequence is newer, and confirmed with the status call while a listing is new.
    • Onboarding: ifinsta's signed attestation (EdDSA or ES256, 15 minutes, single use per onboarding) is verified before every send; the PSP names the merchant's own account; a suspended or closed account stops new attempts while refunds still run.
    • Daily statements are fetched and reconciled line by line; a statement that does not add up is refused, an unmatched line or a late collection is reported and never becomes a payment, a reversal is its own append-only fact.
    • Conformance suite: forty-odd checks across thirteen areas run against a listing's test origin and write a report signed by pay and pinned to the listing and its declaration. The reference PSP in the sandbox passes it; nine deliberate defects (unsigned requests accepted, floats, no idempotency, and so on) each fail it.
    • Loose ends closed: a per-listing, per-day usage statement derived by pay (GET /v1/platform/listings/{id}/usage); unanswered onboardings are re-sent by the scheduler under the same key; a reversal is its own contract event, attempt.reversed; a listing may declare statement_ready_hour; the merchant is told of a late collection and of a scheduled or started suspension, with the other live PSPs named; each market names the regulators it accepts and an approver confirms a live licence again every 365 days; a live-mode payment and the origin refusals have stories.
    • Contract 1.0.0-rc.4 (additive: attempt.reversed, the provider schemas, the listing state machine); pay-contract check-breaking reports it as additive, so no consumer sign-off is needed. CODEOWNERS review and the tag remain with the owners.
    • New connectors/psp_native, contract/providers/ (schemas, OpenAPI, vocabularies, listing state machine) and contract/fixtures/providers/; back-office pages PSP partners, a dashboard page Payment providers; [[dev_psps]] makes the reference PSP live on a local stack.
  • Add the Rust credits wallet core: immutable product and charge-rule versions, atomic double-entry postings, sixteen internal shards per mode, funding lots, unpaid settlement, holds, purchase/refund integration, bounded reversals, identity-based maker-checker, market closes, signed statements and wallet event delivery. The dashboard and back office expose service credits; development pricing is illustrative and money products remain refused. See docs/plans/wallet-core-mvp.md.
  • The payer pages no longer have a bar across the top. Each sheet opens with an identity row instead (the merchant's logo or initial in its brand colour, the line of text, and the Test mode chip when no real money moves), shared through partials/pay.html. Covers the checkout (all five screens), the receipt and the payment link.
  • One shared sidebar (partials/shell.html) for the back office and the merchant dashboard, replacing two copies, and no top bar: brand and environment on top, navigation in the middle, and an account footer (avatar, name, a menu with Appearance and Sign out, and the collapse button), modelled on ifinsta's global sidebar. Appearance (Auto, Light, Dark, High contrast) is a new per-browser choice applied before paint by theme.js. The folded rail shows the environment as a dot; phones get a slim head over the tab strip. check_ui refuses a copy of the sidebar.
  • Two page grounds: the back office and dashboard sit on a lighter --paper-work (#f1f4f8, card vs page 1.10:1) because their cards already fill most of the screen and the stronger step was too heavy; the sparse payer, sign-in and home pages keep the darker --paper (#e9edf3, 1.17:1) that survives a bright room.
  • Page art: a crisp dot lattice in the accent colour, faded from the top, pure CSS (no image, follows the theme tokens). Full field behind the payer pages, sign-in and home; a short band behind the page head in the back office and dashboard; off on phones and in high contrast.
  • Light-room fix: cards now read against the page in a bright room. The page is #e9edf3 (was #f5f7fa), card edges #d5dbe4, input edges #858fa1 (3:1 on a card, was 1.4:1); dark theme gets a darker page and firmer edges to match. Card vs page goes from 1.06:1 to 1.15:1 under glare. scripts/ui-glare.py measures it for every page.
  • Every page now follows ifinsta's property kit on Smooth surfaces: no box shadows, one grouped card per section with hairline rows, a hero for the key number (back-office and dashboard overview, payer sheet), stat figures as one hairline strip, mono tabular money, status as a tinted word, a translucent header, restrained 4 / 6 / 8 / 12 px corners. On a phone the cards dissolve into one flat surface divided by hairlines. check_ui now refuses any box shadow other than the focus halo.
  • The look is now ifinmail's Smooth theme: pale 1 px hairlines in place of heavy borders, one soft shadow, 10/14/18 px corners, pill status, quiet sentence-case labels. Inter and JetBrains Mono are shipped from /static/fonts/ (they were named but never loaded, so every page had been showing the system font).
  • Runbooks render as headings, lists, bold and code instead of raw markdown; long tables keep their words whole; "Enable live money" is no longer styled as a destructive action.
  • check_ui in pay-tools check: no thick edges, no literal colours or pixel radii in components, no class a page uses that pay.css lacks, no inline style, no missing font file.
  • scripts/ui-shots.py and the ignored test a33_pages::dump_every_page render every page (payer, back office, dashboard) in light, dark, phone and high contrast, for looking at before and after a change.
  • .claude/skills/pay-ui rewritten for the Smooth look.
  • Guards on merchants and keys:
    • a business name that borrows ifinsta's, a provider's or an authority's is held for an operator, and blocks live money until cleared (NC-409);
    • a signed notice from a secret-scanning partner revokes a leaked key at once and tells the merchant (NC-490);
    • a key used from many addresses raises one alert an hour and keeps working (NC-060);
    • credentials whose provider names another business's account are flagged for review (NC-377).
  • Launch and operations:
    • a market takes live money only with counsel's residency confirmation recorded (US-260, NC-499), and every market has a rehearsal merchant that is never live (US-263);
    • rotate-keys reseals every stored secret under a new master key without opening any, with token_key keeping issued links stable (NC-387);
    • check-state reports records that contradict each other, for use after a failure drill (US-240);
    • check_migrations_additive refuses a migration the previous release could not run against, so a rollback stays safe (US-215);
    • the callback pool is separate from the page pool (US-214, NC-455).
  • Clippy runs clean with warnings as errors.
  • Twelve defects from the stories review fixed, each proven by a test named for its cases:
    • webhook URLs are checked against private ranges (NC-304, NC-481);
    • refunds need an active merchant (NC-069);
    • rate limits per key, page client and callback connection (NC-072);
    • link caps are held inside the attempt's transaction (NC-038, NC-405, NC-406);
    • a payer can leave an abandoned attempt once the provider confirms the cancellation (NC-107, NC-126);
    • a checkout no method can pay is refused (NC-005, NC-014, NC-019);
    • only verified callbacks are evidence (NC-208);
    • a success with no amount evidence is held (NC-124);
    • early callbacks are adopted (NC-115);
    • pending refunds are queried (NC-281);
    • connections stay on their pinned plugin version (NC-191);
    • a callback that echoes the engine's reference finds an attempt whose initiate timed out (NC-116).
  • simulated 1.2.0: webhook.our_reference.
  • The edge:
    • the error envelope on every response, including 404, 405 and 413;
    • refusal of keys in URLs;
    • contract version checking;
    • a 256 KB body limit.
  • The story suite in backend/server/tests/stories/, one module per area of the stories document, replacing tests/e2e.rs.
  • The check_traceability gate in pay-tools check.
  • A public site for the people who have not signed in: landing, features, pricing, docs, changelog, about, contact, terms, privacy and security, in one shell (site/base.html, one pay.css section). The changelog page is this file, read live with its mtime remembered, so page and file cannot disagree. The error pages are branded per status (error_status), and rate-limit refusals on pages are the branded 429 with Retry-After while /v1/ and /hooks/ keep the JSON envelope.

0.1.0 — first working build

Latest
  • The engine: checkouts, attempts and refunds on contract state machines; idempotent, strict API; the outbox with signed, retried webhook deliveries; deterministic hosted-page tokens; the error envelope with request ids.
  • The config-driven plugin runtime, executing ifinsta.plugin/v1 documents with:
    • auth strategies and a token cache;
    • templates, conditions and computed values;
    • budgets that make a timeout unknown, and total status mapping;
    • the amount check, redaction, evidence capture and egress control;
    • signed or query-confirmed callbacks, and provisioning.
  • Plugins: mpesa 1.3.5, paystack 2.0.2 and simulated 1.0.0.
  • Partners: one partner credential acting through Pay-Account; a platform API to onboard merchants, connect them, install shared credentials, register one webhook endpoint and replay events.
  • Hosted pages for checkout, return, payment confirmation and reusable links. Push and instructions flows work without scripts.
  • An operator console with overview, payments with full trace, merchants, partners, plugins, webhooks, alerts, kill switches and audit.
  • A sandbox with simulated Daraja, Paystack and generic providers, and a simulated ifinsta.com partner.
  • Seven architecture gates, each proven by a bad input, and an end-to-end test across the whole flow.