Closes a credential leak, clears the three scraper bugs that blocked promoting beta to stable, and lands two outside contributions.
Security
Notification channel credentials are no longer returned by the API (#272). GET /settings/notifications answered with every channel's bot token, webhook URL, app token, password and header document in plaintext — so they reached response bodies, browser devtools, HAR exports, and anywhere the response was logged or proxied. The PUT response leaked them too, straight from the repository's RETURNING clause.
Each is now returned as null with a has_<field> flag beside it, the way the auth-config endpoint already answered. In Settings → Notifications a configured credential shows as (set) with an empty field: leave it empty to keep the stored value, type a new one to replace it, or use Clear to remove it.
Reported by @arbrick while writing the Apprise channel — and reported separately rather than folded in quietly, which is why it was fixed once across every channel instead of special-cased per channel.
Scraper — the three that blocked a stable release
Wedged sessions are retired (#254). A session broken by an aborted scrape or a CDP protocol error stayed in the warm pool, so the next request for the same proxy and User-Agent inherited it and failed with Network.enable timed out until the 10-minute idle timer expired. Underneath that sat a worse one: closing a wedged browser sent Browser.close over the same dead connection and blocked for Puppeteer's full 180-second protocolTimeout, inside the request's finally.
The challenge auto-resolve loop gets a reserved budget and polls (#256). It now has a slice the hydration delay and selector wait cannot spend, and returns the instant a challenge clears rather than sleeping blind — which makes a third attempt free on a page that resolves.
Worth recording: the reported cause — the navigation split starving the loop — did not hold against the reporter's own trace. Both rounds ran to completion and the budget-spent warning never fired. Two rounds of eight seconds was simply too short for Turnstile to settle. Both the stated cause and the real one are fixed.
An unresolved challenge wall is no longer logged as a success (#255), so the scraper and the backend stop contradicting each other.
Contributed
- Webhook payload templates are valid JSON for any product name (#268) — values containing
",\, newlines or$&no longer corrupt the body, and an invalid template fails closed rather than POSTing garbage asapplication/json. - The Headers and Payload Template fields are back on the Generic Webhook card. They had been invisible since the v2 import, even though the columns and the v1
webhook_body_templaterename were carried forward the whole time — so a v1 user's custom template was migrated and then became unreachable. - A native Apprise channel (#270), keyed or stateless.
All three from @arbrick.
Upgrading
ghcr.io/mikeknight85/pricestalker-backend:2.2.0-beta.2
ghcr.io/mikeknight85/pricestalker-frontend:2.2.0-beta.2
ghcr.io/mikeknight85/pricestalker-scraper:2.2.0-beta.2
No schema change since beta.1 — migration 024 is still the highest, so upgrading is a tag pull and rolling back is re-pinning it. Nothing here requires action: the credential fix changes what the API returns, not what is stored, so existing credentials keep working.
This is a prerelease. It publishes :2.2.0-beta.2 and moves :beta; stable and latest stay where they are.
What to watch for
The scraper fixes are the reason to run this. Two new log lines carry the signal:
Success after challenge auto-resolve— a bot wall that cleared. This is the line that says the adaptive wait is working.Unresolved challenge wall returned— one that didn't, now a warning instead ofSuccess.
If you only ever see the second on a Cloudflare, DataDome or PerimeterX retailer, the window needs to be longer — interstitialRetries and interstitialWaitMs are per-request options that can be raised without a rebuild.
If you are coming from 2.1.x or earlier
See the 2.2.0-beta.1 notes: that release added token scopes, three migrations, and an SSRF guard that refuses loopback and link-local URLs (ALLOW_INTERNAL_SCRAPING=true if you need them). It also added a tini entrypoint to the scraper image, so recreate that container rather than restarting it if your stack pins its own Entrypoint, Command or User.