v2.0.0
WordPress & WooCommerce plugin, BYOK, free file parsing, hardened security
New: official WordPress & WooCommerce plugin
A first-party WordPress + WooCommerce plugin now ships with every Blengi deployment. Drop it on any WordPress 6.4+ install running PHP 7.4 or newer, paste a workspace API token, and the streaming chat widget loads on every public page — with content sync, product cards, coupon application, order lookup, and lead mirroring out of the box.
Widget embed. One asynchronous <script> tag injected into wp_footer, scoped to the post types you opt into. Hidden on wp-admin, login, AJAX, REST, XML-RPC, and cron paths. Emits data-page-dir and data-page-locale attributes so the bar mirrors correctly on Arabic, Hebrew, Persian, and Urdu locales.
Content sync. Bulk + delta ingest of posts, pages, custom post types, and WooCommerce products as Blengi knowledge sources. SHA- 256 content hash short-circuits re-indexing on unchanged content, so re-running "Sync now" against a stable site is effectively free.
Page-builder rendering. Elementor, Beaver Builder, Oxygen, and Bricks pages are rendered with the builder's own native API before sync, so the synced HTML reflects what visitors actually see. Divi posts route through the_content with setup_postdata primed so third-party filter callbacks see a valid $post global. A pitchbar_post_content_html filter lets sites post-process the final HTML (strip nav chrome, force a custom template, redact a section) without forking the plugin.
Resumable sync. Each sync pass enforces a 20-second wall-clock budget. When a large site can't finish in one tick, the plugin persists a resume marker as a WordPress transient and schedules a WP-Cron continuation 30 seconds out. Shared hosting with a 30s max_execution_time cap no longer fatals mid-sync. The Plugins admin shows a soft notice while a chunked sync is in flight.
WooCommerce shopper context. When a logged-in WC customer visits a page, the plugin attaches a signed data-shopper-token to the widget script tag. The widget forwards it to /widget/init; Blengi verifies it and bakes wp_user_id + email_hash (never plaintext) claims into the chat session.
Order lookup tool. EcommercePreset's order_status capability maps to a real tool. When the visitor asks about orders, the LLM calls lookup_order(limit, order_number?), which POSTs to /wp-json/pitchbar/v1/orders/lookup over HMAC-signed callback. Returns id, number, status, total, currency, items, tracking URL (AfterShip / ST tracking / generic meta), and the customer's order view URL.
Coupon emission & apply. The plugin's CouponSyncer snapshots publish-status shop_coupon entries after every product sync (max 50), filters expired / over-limit, and posts them to Blengi. The LLM emits <coupon code="WELCOME10" .../> markers in chat replies; the widget renders them as cards with Copy and Apply buttons. Apply stages the code in a 15-minute transient that auto-applies on the visitor's next cart load via the woocommerce_load_cart_from_session hook.
Abandoned cart re-engagement. A new BehaviorRule.kind = "abandoned_cart" lets you re-engage a visitor whose cart has been idle. The plugin's cart-state.js mirrors every WooCommerce added_to_cart / removed_from_cart event into localStorage; the widget polls every 30 seconds and fires when the cart sits past the rule's idle_minutes threshold.
Lead push-back. Every Blengi lead is mirrored to WordPress as a WC customer (when Woo is active) or WP subscriber, with pitchbar_lead_id + pitchbar_conversation_id in user meta so the store owner can correlate. Idempotent on email — re-pushes update, never duplicate.
Workspace API tokens. New /settings/api-tokens admin page issues workspace-scoped bearer tokens with granular abilities (the plugin uses wp:integration). Blengi stores only the SHA-256 hash of the plaintext; revoke instantly invalidates without deleting audit history. Cross-workspace revoke is forbidden.
HMAC signed callbacks. Blengi → plugin calls (order lookup, coupon apply, lead push) are signed with HMAC-SHA256 over the raw body using a per-token shopper_signing_secret. 5-minute replay window. The plugin verifies via constant-time hash_equals.
Super-admin plugin distribution. New /admin/integrations/wordpress page lets super-admins build and download the install-ready zip directly from the Blengi admin. Backed by the new php artisan pitchbar:build-wp-plugin Artisan command.
Improvements
Plugin-load-order race fixed. v1.x of the plugin trusted class_exists('WooCommerce') at plugins_loaded priority 10, which silently disabled WooCommerce features when alphabetical plugin order put pitchbar before woocommerce. v2.0.0 defers every WC-dependent registration to the woocommerce_loaded action (with a did_action fallback for the priority-20+ case), so the race is eliminated.
Coupon enumeration via `shop_coupon` CPT. Replaces the v1.x reliance on wc_get_coupons() (not public WC API on every release) with get_posts(['post_type' => 'shop_coupon']) + new WC_Coupon($id). Works on every WC version since 2.0.
`abandoned_cart` admin trigger. Backend already supported it; the agent Behavior triggers admin page now exposes it in the kind dropdown.
Comprehensive WordPress docs. Seven new documentation pages under the "WordPress & WooCommerce" group: Overview, Install & connect, Content sync, Page builders, WooCommerce deep links, REST API reference, Troubleshooting.
Fixes
Page-builder content reaches the RAG store. Pre-v2.0 the plugin called apply_filters('the_content', $post->post_content) without priming setup_postdata, which made plenty of the_content callbacks early-return on is_null($post). The Elementor / Bricks / Oxygen path was worse — those builders don't store layout in post_content at all, so the synced HTML was empty. The fix detects each builder via postmeta and calls its native renderer.
Sync survives shared hosting timeouts. The 30-second PHP execution cap on shared hosts no longer fatals mid-sync on large catalogs. The resumable sync pattern (transient + WP-Cron continuation) lets a 5,000-post sync finish over multiple ticks.
Conversation cookie lifecycle. The widget writes pitchbar_conv_id on init success so the plugin's CartCouponController::applyPendingCoupon hook can locate the correct staged coupon. Pre-v2.0 the cookie was only set when ?pitchbar_conv arrived as a query parameter, which left chat→cart flows without a correlation key.
New: BYOK (Bring Your Own Keys)
Each workspace can now pay its own Cloudflare / OpenAI / OpenRouter / Qdrant bill. Super-admin flips a global toggle in Settings → System; customers paste their keys at Settings → AI keys. Per-user override (force-on / force-deny / inherit-global) lets you grant or block individual users independent of the global flag.
Strategic for operators reselling Blengi as a SaaS — the more end-customers scale, the less you owe upstream. Workspace-level credentials are encrypted at rest via APP_KEY (workspaces.byok_keys is an encrypted:array cast). Five layers of tenant isolation guarantee workspace A's keys never leak to workspace B's container resolution. See /documentation/byok for the full matrix.
New: file parsing routed through Cloudflare's free toMarkdown
PDF, DOCX, DOC, XLSX, XLS, ODT, ODS uploads now flow through Cloudflare Workers AI's toMarkdown endpoint instead of the in-process Smalot / PhpWord parsers. Cost: zero (toMarkdown for document formats consumes 0 Neurons). Quality: structured markdown out — headings, lists, tables preserved, which feeds the chunker much better than Smalot's best-effort plain text on Word-exported or scanned-with-text-layer PDFs. CSV / MD / TXT stay on the local parsers; HTML / SVG uploads are rejected at the validator. Falls back to Smalot when Cloudflare credentials are absent (BYOK OpenAI customers, fresh installs) or when the Cloudflare call fails.
New: sitemap fan-out + 500-page cap
The default services.crawl.max_pages_per_source bumped from 25 → 500. Buyers adding a 100-URL sitemap silently lost 75 pages. SitemapDiscoverer now handles three input shapes correctly: domain root (probes /sitemap.xml + /sitemap_index.xml), direct sitemap URL (fetched verbatim — previous code appended a second /sitemap.xml), and <sitemapindex> recursion. CMSes that emit a sitemap-index by default (WordPress, Shopify, Webflow) get every page indexed instead of just the homepage. Override the cap via CRAWL_MAX_PAGES_PER_SOURCE in env.
New: always-visible "Talk to a human" button
The widget header carries the button at all times for any agent whose vertical includes the ticket_escalation capability (every preset ships with it enabled by default). Plus HumanIntentDetector matches ~40 intent phrases ("talk to a human", "connect me to an agent", "I need to speak with someone", …) in chat messages and short-circuits the LLM straight into the handoff flow.
New: 2-minute wait for human handoff + notifications
Clicking "Talk to a human" used to fall straight to "no one's around" when no operator was actively monitoring the dashboard. Now: the conversation ALWAYS queues and notifies, the visitor waits up to 2 minutes (WAIT_TIMEOUT_SECONDS = 120), and every workspace member with live_chat_available=true gets a database + mail notification (HumanRequestedNotification). Operators who had the dashboard closed still see the queued conversation in the sidebar on next sign-in.
New: public KB articles
Curated answers can now be published as public knowledge-base pages at /kb/{workspace.slug}/{article.slug}. Each curated answer row carries kb_title + kb_published toggle and an inline publish pill in the admin grid. KB pages render operator-supplied markdown through a hardened converter (App\Support\SafeMarkdown) — raw HTML stripped, javascript: / vbscript: / data: URIs stripped from links.
New: Google Sheets as a knowledge source
Pick a sheet tab, the crawler ingests every row and treats each as a chunk seed. Mirrors the Notion / Google Docs OAuth path.
New: multi-currency + lifetime-deal pricing
Visitors on the marketing pricing page can pick their currency; Stripe Checkout uses the matching price. Lifetime-deal plans render as a separate section under the subscription tiers.
Security hardening
Stored XSS on public KB pages — blocked. Illuminate\Support\Str::markdown defaults to html_input='allow' and allow_unsafe_links=true, which would make <img onerror=...> and [click](javascript:...) in a KB article execute as live JS on every visitor browser. New App\Support\SafeMarkdown helper swaps in html_input='strip' + allow_unsafe_links=false. Ten unit tests pin every payload.
SSRF guard on the crawler. A workspace admin pasting http://169.254.169.254/... (AWS metadata), http://localhost:6379/ (loopback Redis), or http://10.0.0.5/admin (intranet) used to walk straight into the server's internal network on the PlainHttpCrawler fallback. New App\Support\UrlSafetyGuard blocks every private / loopback / link-local CIDR, refuses non-http(s) schemes, and (on crawl jobs) DNS-resolves hostnames to catch rebind attacks where attacker.com points at 127.0.0.1. PlainHttpCrawler also re-validates every redirect hop and disables auto-follow.
Widget Origin re-check on every privileged endpoint. New VerifyWidgetOrigin middleware re-validates the request Origin against the JWT-bound agent's allowed_origins on every /v1/widget/* endpoint except init (which enforces inline before issuing the JWT). Defence-in-depth against stolen JWTs replayed from an unlisted origin.
Browser-level defence headers on every web response. CSP (default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'), HSTS on HTTPS requests (1 year + subdomains), X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin. Scoped to the web middleware group — the widget API is excluded because buyers embed cross-origin.
Upload MIME allowlist. UploadController requires mimes:pdf,docx,doc,xlsx,xls,csv,md,markdown,txt,odt,ods on every uploaded file. .html / .exe / .svg / .zip uploads get a 422 at the validator before the parser sees a byte.
Mass-assignment cleanup on `User`. byok_enabled and default_workspace_id removed from User::#[Fillable]. Both are privilege-bearing — sanctioned admin paths use forceFill after authorisation.
Improvements
Crawler retry policy. CrawlPageJob now distinguishes rate-limited (release-with-fresh-backoff, no retry slot burned), permanent (DNS error, 4xx) — short-circuit via fail() — and transient (5xx) failures. Stops the MaxAttemptsExceededException flood that used to fill production logs whenever a customer indexed a domain with a few dead URLs. Per-job timeout 90s, failOnTimeout=true so timeouts still flip Source rows to failed with customer-readable errors.
Cloudflare Worker cron driver hardened. TickCommand --max-time bumped 25 → 55s (the loop exits before the 60s tick boundary), --job-timeout default 120s, queues processed: crawl,index,default. Resolves the MaxAttemptsExceededException flood reported in production.
PDF reindex works for uploads. UploadController now persists parsed segment text under storage/app/private/uploads/{source_id}/segment-N.txt. The Reindex button reads it back without re-uploading the original. Pre-fix, "Reindex" was a no-op on file uploads because the controller short-circuited on a null document.url.
PDF parser sanitization. Strips PDF stream artifacts, control bytes, and "/Type /Page" leakage that some encoders dump into getText(). Falls back to full-document text when per-page extraction returns empty (common for Word-exported PDFs). Drops segments with <75% printable characters as "no extractable text" instead of indexing garbage.
`pages_total` in source preview reads `services.crawl.max_pages_per_source`. The progress indicator on the Sources preview now reflects the current cap (500 by default) instead of the hard-coded 25 that was stale after the bump.
Cloudflare AI Gateway support. Set CLOUDFLARE_AI_GATEWAY_URL to route Workers AI calls through Cloudflare's gateway — observability, caching, optional per-account rate limits.
Documentation refresh. New /documentation/byok page; security, knowledge, live-chat, allowed-origins, env, and deployment pages all updated to reflect the v2.0.0 changes.
Fixes (operator-reported during v2.0.0)
"AI keys" link surfaced to customers. After a buyer enrolled BYOK from the admin dashboard, their customers had no way to find the /settings/byok-keys form because no nav entry pointed at it. Fixed by adding a conditional sidebar entry driven by the byok.unlocked Inertia shared prop.
"AI keys" link hidden from super_admin. Super-admins were seeing the customer-facing BYOK menu. They manage platform-wide AI credentials at /settings/system instead; the workspace BYOK form is customer-only. The controller hard-404s for super_admin too as belt-and-braces.
Token-count cap precision. Replaced naive mb_strlen / 4 heuristics for the max-tokens budget with a Unicode-aware estimator that distinguishes ASCII (4 chars/token) from CJK (~1 char/token). The plan's max_tokens ceiling now applies correctly to non-English visitor messages.
Sources errors are sanitised before reaching the customer UI. Raw Cloudflare 401 JSON envelopes used to bleed through to the Sources list. SourceErrorPresenter now rewrites them to short friendly lines.

