# Changelog

All notable changes to the qID Connect integration surface. Versions apply
to `@qid/connect-server` and `@qid/connect-widget` together.

## 1.5.5 — 2026-07-22

- **Works-with rows can link to the wallet.** A wallet entry can now carry a
  `url`; the row becomes a link — clicking the name or the status badge opens
  the wallet's page in a new tab, the intuitive move for someone who does not
  have it yet. The defaults now link **PQ Wallet for BTX → pq-wallet.com** and
  **bonuz Wallet → bonuz.xyz/btx-wallet**. Server SDK byte-identical.

## 1.5.4 — 2026-07-22

Naming: the desktop wallet is now shown as **PQ Wallet for BTX** everywhere in
the dialog, docs, and site (was "BTX PQ Wallet"). The works-with list, the
deep-link button, the paste instructions, and the captions all updated; server
SDK byte-identical to 1.5.2, carries all the 1.5.3 fixes.

## 1.5.3 — 2026-07-22

Widget-only (server SDK byte-identical to 1.5.2). Two dead-feeling controls in
the "Open in wallet" transport now react, surfaced by a live btc2btx integration.

- **The deep-link button gave no feedback on click.** It fired the
  `btxqid://` scheme and then changed nothing for ~5s — and when the OS has no
  handler registered (the common "nothing happened" case) nothing ever changed,
  so the button read as broken. It now acknowledges the click immediately
  (briefly disabled with an "Opening…" label, then re-enabled with an "…again"
  label), and the download/copy fallback affordances now appear in
  ~2.5s instead of 5s so there is always a fast next step.
- **The "i" help affordance did nothing in embedded webviews.** It was a bare
  `target="_blank"` link — it navigated away from the sign-in flow in a browser
  and was a complete no-op inside in-app webviews (the bonuz app, embedded
  browsers), where the help is most needed. It now toggles a one-line inline
  hint in-flow (a guaranteed reaction everywhere) that still links out to the
  full guide.

## 1.5.2 — 2026-07-21

- **bonuz Wallet is live.** The dialog's "works with" list now shows **bonuz
  Wallet — Available now** (mobile): scan the qID QR with the bonuz app, pick
  your wallet, and you are in. Hosted-widget integrators get the change
  automatically; vendored copies pick it up on the next pull.

A fresh-eyes review from a different model family (8 lenses over the whole
surface, every finding independently refuted) found five real medium defects the
prior rounds missed — all in async-race discipline and adapter unhappy-path
parity, none in the crypto core. All fixed and live-verified; the frozen protocol
is untouched.

- **Rotation could orphan an accepted QR sign-in (medium).** A proof accepted in
  the ~1.5s before a ~105s rotation was parked under the old nonce, but after
  rotating the widget only ever polled the new one: the wallet said "signed in"
  while the browser showed a fresh QR forever. `renewSession` now does one
  last-chance poll of the old nonce (with its poll secret) before discarding it,
  and finishes the sign-in if a proof landed. Covered by a new server test.
- **Log out reported success on failure (medium).** `doLogout` swallowed a failed
  request and never checked `res.ok`, then cleared the chip and fired
  `onSignedOut` — a shared-machine hazard, since the server session was still
  valid. It now keeps the chip and shows an inline "Couldn't log out — retry" on
  failure, only signs out on a confirmed 200, and bounds the request with a 15s
  timeout so a hung POST can't freeze the account widget.
- **A close-and-reopen could let a stale rotation hijack the new dialog
  (medium).** `renewSession`, the paste-verify continuation, `startPolling`, and
  the soft-retry timer now all guard on the `open()` generation, so an in-flight
  rotation from a closed dialog can't clear the live dialog's poll timer or poll
  the wrong nonce.
- **`requireQidSession` could crash the process (medium).** A throwing accounts
  adapter (a transient DB outage) escaped as an unhandled rejection — a hung
  request plus a process crash on Express 4 / Node ≥15. It now mirrors the
  middleware's try/catch and returns 500 `server_error`. New test.
- **The Next adapter had no body-size cap (medium).** Express rejects bodies over
  64 KB; Next buffered `request.json()` unbounded, a memory-exhaustion DoS on the
  cross-origin-reachable `/proof` for any self-hosted runtime without a fronting
  proxy limit. Next now enforces the same 64 KB cap (and an empty POST body now
  matches Express's 401, not a parse 400). New test.

Also hardened (non-blocking, from the same review):

- **Origin is canonicalized** (lowercase host, default port dropped), so a
  mixed-case `origin` config no longer 403s every browser sign-in while QR keeps
  working — the worst kind of asymmetric failure.
- **Nonce retirement is secret-bound.** The rotating widget sends the retired
  nonce's poll secret; a bystander who only saw the QR can no longer burn a
  victim's in-flight sign-in.
- **A lost `done` response is survivable.** A poll claim is now idempotent for the
  matching secret within a 30s grace, so a dropped response on a flaky mobile hop
  can be re-claimed instead of losing the sign-in.
- Cookies-blocked browsers get an honest "allow cookies and try again" instead of
  a "Signed in" screen the next page load contradicts; `verify`/`poll` fetches
  time out like `challenge`; the poll cap is anchored to open time so an abandoned
  tab stops polling; the account chip survives a no-op repaint without dropping
  keyboard focus; a deeplink handoff gets a rotation grace; an unknown poll status
  surfaces via `onError`; a trailing slash in `api` is normalized; SQLite
  `_row` can't be shadowed by a profile key and `patch` strips reserved keys;
  `buttonText`/`pollIntervalMs` and the SQLite TTL rule are documented; the
  no-wallet dev flow in INSTALL.md now says to use the Desktop-wallet tab first.

## 1.5.1 — 2026-07-21

Fixes from an exhaustive adversarial audit of the whole v1.5.0 surface
(7 lenses, every finding independently verified). All confirmed items fixed.

- **Next.js: `apiPath` was missing (HIGH, builder-breaking).** The next-demo
  route and the INTEGRATION.md Next snippet omitted `apiPath: "/api/qid"`, so
  the proof URL baked into the QR and deep link pointed at `/qid/proof` and
  404'd — QR and deep-link sign-in (the default transports) silently failed on
  Next; only copy-paste worked. Added everywhere, with a loud comment.
- **The account-chip doc snippet threw (MEDIUM).** The chip block in
  INTEGRATION.md and INSTALL.md referenced `#signin` but only showed
  `#account`, so a verbatim copy threw. Both now include both divs.
- **Forced-logout CSRF (LOW).** `POST /qid/logout` now carries the same
  login-CSRF (Origin) guard as `/verify` and `/poll`, so a cross-site page
  can't force-log-out a visitor. Covered by a new test.
- The signed-in chip's status dot no longer borrows the "connecting" pulse
  (it's a settled state); `bad_origin` now has a helpful user-facing message
  (was falling back to "server had a problem"); the logout race guard gains a
  belt-and-suspenders `loggingOut` check in `refresh()`; Escape no longer
  double-dismisses when both widgets are open; `onSignedIn` on the account
  chip documents that it fires on restore.
- **/poll CSRF hardening.** The cookie-minting poll now also checks Origin as
  a fallback (mirroring /verify), so the guard no longer fails open if a proxy
  strips `Sec-Fetch-Site`.
- Account dropdown a11y: focus moves into the menu on open and returns to the
  chip on close; the chip survives a transient `/session` blip (only a real
  401 clears it) instead of flapping.
- Doc/version tidy: DESIGN.md updated for three transports + the account chip;
  next-demo README endpoint count; conformance-suite path; the integrate
  page's "Get the code" uses the release-pack download (not a git clone of a
  preview-private repo); CHANGELOG bonuz wording kept to "coming soon"; root
  package.json version aligned to 1.5.1.

## 1.5.0 — 2026-07-21

- **New: the signed-in account chip + dropdown (`mountQidAccount`).** The
  counterpart to the "Sign in" button — a compact chip (green dot + short
  address) that opens a dropdown with the full address (click to copy), an
  optional explorer link, extensible integrator menu items, and Log out.
  Log out is now a deliberate two-step (open the menu, then Log out) instead
  of a single accidental click. Same `--qidc-*` theming as the dialog, so the
  signed-in state looks identical on every BTX app. Renders nothing when
  signed out; call `refresh()` after sign-in to show it without a reload.
  The `menuItems` slot is the extension point for future Bonuz ID / social
  data.
  - Hardened via an adversarial review before release: a request-generation
    guard so an in-flight `/session` response can't resurrect the chip after
    Log out, `destroyed` re-checked after every await, explorer link closes
    the menu, logout is double-click-guarded, and keyboard-focusable copy.

## 1.4.2 — 2026-07-17

Fast-follows from a second adversarial review (of the v1.4.1 fixes). Verdict was
SHIP CLEAN; these close the two residual low/nit edges so the release has none.

- `fetchChallenge` now has a 20s AbortController timeout, so a hung request
  (black-holed TCP, captive portal) rejects instead of hanging — a never-settling
  fetch can no longer latch the renew re-entrancy guard and block further renews.
- Focus restore on a re-open (the "Try again" retry path) now returns focus to the
  original trigger instead of falling to `<body>`.

## 1.4.1 — 2026-07-17

Fixes from an adversarial multi-agent code review of v1.4.0 (3 blocking + polish).

- **Orphaned retry timer could burn a live nonce (HIGH regression).** After a
  failed background rotation armed a soft-retry, a subsequent successful renew
  did not cancel it, so ~15s later it retired the *current* nonce — silently
  breaking an in-flight sign-in. renewSession now cancels the pending retry on
  entry and is re-entrancy guarded.
- **Focus trap could escape the modal (HIGH regression).** The focusable set
  included disabled and hidden controls (the disabled Verify button, the hidden
  deep-link fallback), so Tab could walk out of an aria-modal dialog. The set is
  now filtered to genuinely tabbable elements.
- **onSignedIn could fire for a superseded dialog (HIGH).** The open() generation
  token is now also checked in the poll loop, so an abandoned generation's
  in-flight poll can't drive success after the user moved on.
- Re-opening the dialog no longer emits a spurious onClose; unmount() is final
  (open() no-ops after it); the deep-link fallback timer is cleared on tab
  switch; the signed-in address renders via textContent.

## 1.4.0 — 2026-07-17

Field-driven release: everything below came from taking qID Connect all the
way to production on a real static site (btc2btx.com) plus a UX review.

### Widget — reliability
- **Paste flow is exempt from proactive rotation.** Copy → wallet → sign →
  paste realistically outlasts the ~2-minute display life; rotation used to
  wipe the textarea and retire the request the user had just signed. The
  paste tab no longer rotates (and drops the confusing "Refreshes in Ns"
  line); the server TTL still bounds it. Switching to a QR/deeplink tab with
  a stale request renews first.
- **A failed background rotation keeps the dialog open** and soft-retries
  with backoff (plus a manual "tap to retry") instead of closing the dialog
  out from under the user.
- **An unreachable backend at open shows an in-dialog error** ("Sign-in is
  temporarily unavailable" + Try again) instead of flash-opening then
  vanishing.
- **A real server outage no longer looks like "waiting for wallet"** — after
  a few failed polls the wait line says "Trouble reaching the server — still
  trying" and recovers on success.
- Terminal-state guards: rotation can't wipe the "Signed in" screen; a
  double-clicked open can't corrupt timer state (generation token).

### Widget — API
- Returns `{ open, close, unmount }`; `trigger: false` renders no button so
  you can open the dialog from your own UI.
- `onClose(reason)` — `'backdrop' | 'escape' | 'x' | 'signedin' | 'programmatic'`.
- `onError(err)` is now structured: `{ stage: 'challenge'|'verify'|'poll',
  code?, httpStatus?, cause? }`, and verify verdicts (`bad_signature`,
  `nonce_used`, …) reach it too — real telemetry on why sign-ins fail.

### Widget — UX & a11y
- The **Copy** button is framed and gently pulses so it reads as the action.
- A round **ⓘ info button** on every transport links to the new
  https://qid.dev/connect/how (per-transport how-to, incl. the honest
  notarization note for "Open in wallet").
- The QR center mark is now a small **qID** wordmark, not a lens.
- Focus is moved into the modal on open, **Tab is trapped**, focus is
  restored to the trigger on close; arrow-key tab navigation; `prefers-
  reduced-motion` disables the entrance/pulse animations; `--qidc-anim-
  duration` hook; per-mount `--qidc-*` theming now reaches the portaled
  dialog too.

### Docs & examples
- **New `examples/static-rewrite-demo/`**: a standalone `node:http`/bun
  server (durable SQLite, boot-time origin sanity check, hardened systemd
  unit) for the "CDN page + backend on another host + same-origin rewrite"
  pattern, with rewrite recipes for Vercel / Netlify / Caddy / nginx.
- INTEGRATION.md: the apex/www (and http↔https) **origin trap** callout; the
  "do not proxy" rule reworded into ❌ BAD / ✅ GOOD (a same-origin `/qid/*`
  rewrite is fine and is **required** for the first-party session cookie when
  your server is on another host); `unzip`/Node-SQLite prerequisite note.
- DESIGN.md: theming and the portaled dialog explained.

## 1.3.3 — 2026-07-17

- **Field hardening from the first real integrations.**
  - The QR no longer dies on "server amnesia": if the server reports a
    request expired seconds after issuing it (a restart, or multiple
    instances without a shared store), the widget auto-recovers once with
    a fresh challenge and reports the exact diagnosis through `onError`.
  - A failed rotation no longer closes the dialog — it offers
    "Can't reach the site — tap to retry".
  - The deep-link tab reveals its fallback options after 5s regardless
    of focus changes (a failed `btxqid://` launch also blurs the page, so
    blur never proved the wallet opened).
  - The QR itself is now click-to-copy.
- Production checklist: explicit multi-instance / shared-store requirement.

## 1.3.2 — 2026-07-17

- The dialog's "works with" list carries **bonuz Wallet (coming soon)** for
  the mobile signer. When it ships, hosted-widget integrators flip to
  "available now" automatically on our deploy — no dApp changes needed.

## 1.3.1 — 2026-07-16

- Fix: the 1.3.0 copy trim dropped `appName` from the dialog — the option
  did nothing. The subtitle now reads "Sign in to <appName> with your BTX
  address. No email, no password." Also corrects the hosted build's
  provenance header (it cited a commit that predated its content).

## 1.3.0 — 2026-07-16

- **QR-first dialog.** The default transports are now
  `["qr", "paste", "deeplink"]`: the dialog opens on a visible QR code,
  WalletConnect style, with desktop copy-paste and one-click "Open in
  wallet" one tap away.
- **Hosted widget channel.** The widget is now also served from
  https://qid.dev/connect/widget.js (latest) and `widget-<version>.js`
  (pinned). Importing the hosted file means wallet-list and UX updates
  reach your users automatically on our deploys; the signed protocol stays
  frozen v1 either way.
- Calmer dialog copy: shorter subtitle, captions, and safety line.
- Brand casing: **bonuz Wallet** in the "works with" list.

## 1.2.0 — 2026-07-16

- **Dynamic connector.** Every sign-in request now has a short display
  lifetime with a visible countdown, rotates itself proactively before it
  goes stale, and on rotation the server burns the superseded nonce
  (`challenge()` accepts `{ retire }`; the Express and Next adapters parse
  the optional body). A screenshotted or stale QR, paste request, or deep
  link stops being acceptable the moment it leaves the screen.
- **`btxqid://` deep-link transport.** One-click connect into the installed
  desktop PQ Wallet for BTX (v0.27+): `transports: ["paste", "qr", "deeplink"]`.
  New exported `buildBtxqidSignUrl` with unit tests.
- Unified "expired — get a new one" affordance on every transport
  (previously the paste tab had none); poll jitter plus a hard
  max-duration cap so an abandoned dialog stops polling.
- `SECURITY.md` — private vulnerability reporting policy.
- Integration guide published: https://qid.dev/connect/integrate
- Tests: rotation covered at the core and over both HTTP adapters.

## 1.1.0 — 2026-07-15

- **Login-CSRF guards** on `/verify` and `/poll` (a cross-site page cannot
  plant a session in a visitor's browser).
- Atomic async replay consume in the nonce store; widget timer cleanup.
- Dialog wallet list ("works with"), signing animation, accounts context
  for adapters.
- Live browser demo (the qid.dev/connect page) and OG cards.

## 1.0.0 — 2026-07-11

- Phase 1: connect, prove, sign in. Server SDK (challenge, verify with the
  frozen qID Sign-In v1 verifier, account by address, sessions, SQLite
  stores), the "Sign in with qID" widget (paste + QR transports, no relay
  server), Express and Next.js adapters, two example apps, the reference
  test signer CLI, integration/migration/design docs.
