feat(core): notify, i18n, and useListState primitives (#131, #132, #133)#140
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Security Review — No High-Confidence Vulnerabilities Found
Reviewed the full diff for PR #140 (notify, i18n, and useListState primitives). No confirmed vulnerabilities were found. Below are the areas examined and the reasoning.
Checklist
| Area | Finding |
|---|---|
| Injection (SQL, command, template, path traversal) | ✅ Clean |
| Authn/authz bypasses | ✅ Not applicable (no auth surface changed) |
| Secrets / token leakage / insecure logging | ✅ Clean |
| Unsafe deserialization / SSRF / XSS / CSRF | ✅ Clean |
| Dependency / supply-chain risk | ✅ No new dependencies introduced |
Detailed Notes
shared/interpolate.ts — {{param}} template engine
The regex \{\{(\w+)\}\} restricts keys to [a-zA-Z0-9_]+. Keys like __proto__ and constructor match \w+, but the access is read-only (params[key] → String(value)). There is no write path, so prototype pollution is not possible through this function. Rendered output is always a plain string; React's JSX renderer escapes it before display, so no XSS concern within the library's own UI.
shared/list-state.ts — URL search-param parsing
parseListStateFromSearchParams returns raw URL string values for type: "string" schema fields with no sanitization. This is appropriate for a client-side URL-state utility — React escapes strings at the JSX render boundary. Consumers who pass these values to dangerouslySetInnerHTML or other raw-HTML sinks would be responsible for their own sanitization, which is standard React practice and not a library-level vulnerability.
context/notify.tsx — notification provider
Messages are forwarded as strings to sonner's toast.* APIs (or a consumer-supplied override). Sonner renders toast content as React children, not raw HTML, so there is no XSS vector in the default path.
scripts/extract-i18n-keys.ts — dev-only file scanner
The script walks a hardcoded path derived from import.meta.dirname (resolved at build time) and emits JSON to stdout. It is a local developer tool, not deployed or invoked by end users. No injection or path-traversal risk.
No new dependencies were added to package.json; sonner was already a declared dependency.
Verdict: No actionable security findings. This PR is clear to merge from a security standpoint.
Sent by Cursor Automation: Find vulnerabilities
Co-authored-by: Cursor <cursoragent@cursor.com>
Address Bugbot feedback: derive next state from getSearchParams() instead of a stale render closure, and re-read params each render so router-driven URL changes are not masked by useMemo caching. Co-authored-by: Cursor <cursoragent@cursor.com>
Multiple setState calls in one event now merge into a single setSearchParams call via microtask batching, and no-op updates skip history entirely, so one back press undoes one user action. Co-authored-by: Cursor <cursoragent@cursor.com>
Move the coalescing queue to module scope so concurrent updates from different useListState instances merge into one setSearchParams call instead of the later flush clobbering the earlier one's params. Co-authored-by: Cursor <cursoragent@cursor.com>
…detection URLSearchParams serialization is order-sensitive and the URL may hold explicit default values, so string equality could miss state-identical updates and push spurious history entries. Compare parsed field values per entry instead, and use order-insensitive comparison for the final net-no-op check. Part of #133 Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 75a1785. Configure here.
…L update Replace per-instance urlVersion state with a module-level store consumed via useSyncExternalStore. Previously only the instances that called setState re-rendered after a flush, so sibling hooks sharing query keys could serve stale state when the router binding does not re-render on search-param changes (e.g. the Next.js preset). The popstate listener moves into the shared store subscription. Part of #133 Co-authored-by: Cursor <cursoragent@cursor.com>



Summary
Core provider primitives for the Refine-inspired simplification effort — plugin adoption is intentionally deferred to #136.
notifyprop onStackProvider,useNotify()hook, sonner default with partial override mergei18nprop onStackProvider,useTranslate()hook with default-value fallback and{{param}}interpolation, plusextract-i18n-keys.tsscript for future sweeps@btst/stack/client) with defaults omitted from URL, field-levelhistory: "replace"|"push", and SSR helperparseListStateFromSearchParams. Same-ticksetStatecalls coalesce into a single history entry, and no-op updates skip history, so one back press undoes one user action. RoutergetSearchParams/setSearchParamswere already shipped in Top-level router adapter on StackProvider (framework presets for Link/navigate/refresh/Image) #127 — no binding changes needed.Part of #131
Part of #132
Part of #133
Test plan
pnpm buildpnpm typecheckpnpm lintpnpm test -- --runNote
Medium Risk
Touches StackProvider composition and global URL/history batching in useListState, which can affect navigation and back-button behavior across list UIs; defaults preserve prior behavior when new props are omitted.
Overview
Introduces core provider primitives for notifications, i18n, and URL-backed list UI state. Plugin wiring is intentionally left for a follow-up (#136).
StackProvidernow accepts optionalnotifyandi18nprops, wrapped in new boundaries around the existing router/auth tree.useNotify()merges partial overrides with sonner defaults.useTranslate()calls a customtranslatewhen provided, otherwise returns English defaults with{{param}}interpolation via sharedinterpolate.useListState(exported from@btst/stack/client) syncs filters/tabs/pagination to the router’sgetSearchParams/setSearchParams, omits default values from the URL, supports per-fieldhistory: "replace"|"push, batches same-tick updates into one navigation, skips no-op history writes, and uses a module store so sibling hook instances stay in sync. SSR can seed state withparseListStateFromSearchParams.Also adds
extract-i18n-keys.tsfor future plugin sweeps, API reference sections for the new hooks, and Vitest coverage for providers, list-state serialization, and hook/router behavior.Reviewed by Cursor Bugbot for commit 12a2313. Bugbot is set up for automated code reviews on this repo. Configure here.