{
  "slug": "bw-lead-ai",
  "name": "BW Lead Attribution Intelligence",
  "version": "1.4.0",
  "download_url": "https://plugins.bowden.works/wp-content/uploads/plugin-updates/bw-lead-ai-1.4.0.zip",
  "download_hash": "sha256:e70afa77ca2f5825805b7e264032bd7d5339896bd2ce3f601a119641b6bb0f5b",
  "download_size": 271062,
  "requires": "6.0",
  "tested": "",
  "requires_php": "7.4",
  "last_updated": "2026-08-05",
  "homepage": "https://plugins.bowden.works/bw-lead-ai/",
  "author": "Bowden Works",
  "description": "Capture traffic source, attribute it to every lead, and understand where your leads are coming from.",
  "changelog": "## [1.4.0] - 2026-08-05\n\n### Added\n- **Cross-domain handoff** — carry a visitor's attribution across to a form on a\n  different domain (an enrolment portal, CRM, booking tool, off-domain checkout).\n  New **Handoff** settings tab, **off by default**.\n\n  A short-lived random token rides on the outbound link. **The journey itself is\n  never in the URL** — no browsing history in the address bar, in browser history,\n  or in the destination's access logs.\n\n- **Two modes**, chosen independently:\n  - **Journey link** (recommended) — only the opaque token crosses domains. The\n    destination stores a link; your team follows it and reads the journey here,\n    signed in to WordPress. **No visitor data leaves the server.**\n  - **Data handoff** — the destination exchanges the token for the datapoints you\n    tick, and fills its own fields. Gated behind an explicit acknowledgment\n    checkbox, because this sends visitor data to a third-party origin.\n\n  Running both is the recommended shape: minimal attribution fields to the CRM so it\n  can report, full journey kept at home behind login.\n\n- **Confirm-to-persist.** A record is minted pending on click and deleted within the\n  token lifetime unless the destination reports a submission. Only journeys a real\n  person produced are kept, so a page with heavy click-through does not accumulate\n  storage.\n\n- **Retention is configurable, including Unlimited.** Lead cycles can run years and a\n  link that dies before the lead converts is a broken feature. Confirmed journeys\n  arrive at roughly the rate of form submissions, so even a decade is a few thousand\n  small rows. The pending lifetime is separate and stays short — that one is an abuse\n  control, not a business setting.\n\n- Endpoints: `POST /wp-json/bw-lead-ai/v1/handoff` (create/refresh, same-origin),\n  `GET …/handoff/<token>` (claim, single-use), `POST …/handoff/<token>/confirm`.\n  Configurable link parameter (default `bwlai`), destination domains, allowed\n  origins, token lifetime and datapoint set. The Handoff tab prints the exact\n  endpoints and journey-link format to hand to whoever manages the destination's\n  tag manager.\n\n### Security\n- Routes are **registered only when the feature is enabled** — with handoff off they\n  do not exist, so a default install's attack surface is unchanged.\n- Tokens are 128-bit from a CSPRNG and **stored only as a sha256 hash**, so a\n  database dump or backup leak contains no usable tokens.\n- The data claim is **single-use**: the first successful read burns it. That token\n  ends up in the destination's URL bar, history, logs and readable by every other\n  script on their page, so the exposure window is one round trip rather than the full\n  lifetime. Retry-on-404 still works, because retries only matter when the first read\n  failed.\n- **CORS is an allow-list, never `*`**, with `Vary: Origin` and `no-store`. Stated\n  plainly in the code: CORS is not authentication — it stops browser JavaScript on\n  other origins and nothing else. Token secrecy is the actual control.\n- The server **filters the payload to the enabled datapoints**, so a tampered front\n  end cannot widen what gets sent. Size caps per field and overall.\n- Rate limited per client without recording who they are: counters are keyed by a\n  salted daily hash, never a stored IP, preserving the plugin's no-IP-logging rule.\n- Expiry is enforced **at read time**, so a late or missing cron run is a housekeeping\n  problem rather than a security one.\n- Missing, expired and already-claimed all return an identical 404, so the endpoint\n  cannot be used to probe which tokens existed.\n- Everything rendered in the journey viewer is escaped on output — UTM values,\n  referrers and page paths are attacker-controlled strings.\n\n### Notes\n- **First custom table**, created lazily when the feature is first enabled — never on\n  activation. Sites that do not use handoff never get it. Dropped on uninstall, along\n  with the prune cron.\n- **No REST nonce on the create endpoint, deliberately.** It runs on public, cacheable\n  pages; a page cache or CDN would serve a stale nonce and the feature would fail\n  silently for a subset of visitors — the exact failure mode this design guards\n  against. It is treated as public and defended with rate limits, payload caps,\n  server-side filtering and a global pending ceiling instead.\n- Debug mode logs every handoff step to the browser console, so a silent failure is\n  diagnosable rather than invisible for weeks.\n- 54 server-side verification checks covering gating, filtering, single-use claim,\n  idempotent confirm, retention including unlimited, prune, read-time expiry, origin\n  normalisation, cross-tab setting persistence and viewer escaping."
}
