Security hardening + 7 bugfixes from npm testing. No new tools, no algorithm changes — purely defensive.
Bugfixes
dsers_find_product — empty keyword rejected. Passing keyword: "" or omitting both keyword and image_url used to return random products. Now throws a clear error.
dsers_find_product — image search respects limit. DSers backend ignores limit on image search; we now truncate client-side and return truncated_from showing the original count.
dsers_find_product — pagination overlap note. Response includes pagination_note when search_after is present, warning that pages may overlap by one item.
dsers_find_product — insufficient results note. When fewer items than limit are returned and there are no more pages, a note field explains the shortfall.
dsers_product_update_rules — persist failure blocks push. If saveDraft() fails, job status is set to persist_failed and dsers_store_push refuses to proceed. Previously the failure was a buried warning and push would silently use old rules.
dsers_store_push — pricing rule transparency. When apply_store_pricing_rule is chosen, the response now shows the store's actual pricing rule configuration instead of silently overriding.
dsers_rules_validate — extreme pricing warnings.multiplier > 100, fixed_markup > $500, and fixed_price > $10,000 now produce warnings with concrete dollar examples.
Security
OAuth callback — redirect_uri restricted to localhost / 127.0.0.1 / [::1]. Authorization code now includes a nonce for replay prevention and client_id binding.
Rules engine — description_override_html and description_append_html validated against <script>, <iframe>, <object>, <embed>, <form>, on* event handlers, and javascript: URLs. Blocked at validation time; sanitized at runtime as defense-in-depth.
CLI OAuth — callback server binds 127.0.0.1 only. state parameter verified on callback. Port fallback (3001 → 3002 → 3003). 5-minute timeout. openBrowser deduplicated via import from browser-finder.ts.
Token storage — PBKDF2 (100k iterations) + random salt replaces bare SHA-256. Salt stored in ~/.dsers-mcp/key-salt. Auto-migrates existing credentials on first load. File written with mode: 0o600 directly (no post-hoc chmod).
Error sanitization — err.body in DSers API errors now strips tokens/keys/session IDs before truncation.
Improvements
DSers client — 30s global request timeout via AbortController. Rate limiter promoted to module-level (shared across instances).
Auth — getSession() refresh deduplicates concurrent callers via a shared promise. invalidate() clears all fields including refreshToken, clientId, oauthBase.
centsToDollars — browse-shared.ts now imports from helpers.ts (was duplicated).
Node — minimum version lowered from >=22 to >=20.
Regression validation
Two independent test accounts (different stores, different product counts). 57 e2e scenarios total, 0 failures. Unit tests: 25 files, 618 tests, all green.
1.5.0 — 2026-04-10
Big release. Finally nailed down the "swap supplier" problem.
New tool: dsers_sku_remap
There was one scenario that kept nagging at me — the old supplier jacks up the price, runs out of stock, or gets delisted, and you want to swap to a new one. You can do this in the DSers dashboard, but clicking through variant by variant is painful, and you still have to figure out which seller variant corresponds to which new supplier SKU. I'd already built a separate sku-matcher engine (option alignment + unit normalization + synonym tables + dHash image similarity scoring) — originally as an internal tool. This release lifts the whole thing into the MCP interface so your AI agent can do the replacement in one sentence.
Two modes:
STRICT — You already have the new supplier URL. Pass new_supplier_url, the tool uses sku-matcher to align variants on both sides, and honors auto_confidence to decide what auto-swaps, what keeps the old supplier, and what gets flagged as unmatched for manual review.
DISCOVER — You only know "this supplier is dead, find me a replacement". Omit the URL. The tool reverse-image-searches the DSers pool from the current product's images, then ranks candidates via a multi-factor score (sku score + image frequency + product rating + store rating + price proximity + stock + order count) and auto-picks the best match.
Both modes are two-step: always call mode='preview' first (read-only) to inspect diffs and pool_additions, then call mode='apply' to actually write.
Key design decisions:
Old suppliers get archived into mapping.pool[] as history, not thrown away. If the new supplier turns out to be a lemon, the next dsers_sku_remap run can reconsider the old supplier as a candidate.
pool is monotonic (grow-only). There's a validator rule specifically enforcing this invariant.
6 structural checks run before any write, covering supplyProductId numeric format, supplyVariantId format, optionId / valueId correspondence, and more — so DSers never gets written garbage.
Auto-detects MapBas vs MapSta mapping type based on option alignment consistency. When alignment is incomplete, it force-downgrades to Standard and surfaces the reason in warnings.
Other tool cleanup
manifest.json's tools array was missing 4 entries — the code had already shipped them but the metadata never caught up. Fixed in this release:
dsers_import_list — browse the import staging list with cost / sell price / stock / markup status
dsers_my_products — view products already pushed to a store (with supplier links for re-import)
dsers_find_product — search the DSers product pool (keyword or image)
dsers_sku_remap — the new one described above
Tool count goes from 9 listed to 13 (matches what tools/list has actually been returning since 1.4.x).
Stability fixes
dsers_sku_remap went through 15 fix rounds from round-1 to round-5.7.5. Headline items:
F1 — Path A candidate fetch now has a product-pool/product/detail fallback. When the user-provided supplier URL is already mapped to another product in the account, DSers import-list reports "already exists" but doesn't return an importListId, which caused the old Path A to time out after 5–26 s. The fallback hits the pool endpoint directly and returns in a few hundred ms.
F2 — skuRemap entry point now runs a store-ownership check first. A shape-valid but wrong store_id no longer travels all the way into Path B rank_candidates before bottoming out with a misleading "no viable match" error.
F3 — MCP-boundary input shape guard. Empty strings / letters / 200-char strings / special symbols for dsers_product_id are now rejected in 0–1 ms without reaching the handler.
F4 — CODEC copy containment. DSers' raw Go-map error text no longer leaks into user-visible errors; everything routes through a clean store_id_mismatch envelope with explicit dsers_store_discover / dsers_my_products remediation hints.
M1 — Production-grade int64 range convergence. The 19-digit numeric validation for store_id / dsers_product_id now adds a BigInt .refine() at the MCP layer that checks against the signed int64 ceiling (9223372036854775807). Overflow values are rejected at the MCP boundary, avoiding the DSers int64 parser bug entirely.
O1 — URL scheme case canonicalization.HTTPS:// / HttP:// etc. are now lowercased at the scheme prefix inside SUPPLIER_URL_SCHEMA.transform(). Host / path / query are left exactly as provided (RFC 3986: scheme is case-insensitive, path is case-sensitive; Alibaba URLs like Widget_ABC_1600123456789.html must survive intact).
O3 — Hard error text hygiene.sanitizePrimaryErrorForWarning strips DSers JSON blobs / Go <nil> / map[] noise from Path A fetch errors and leaves clean prose with a compact (DSers API HTTP NNN) marker for the upstream status code.
O-R4-3 — checkStoreOwnership direct-lookup fast path. Wrong dsers_product_id failure latency drops from ~1.3 s (list-scan) to under 500 ms (getMyProductDetail direct query), in line with wrong store_id latency.
O-R5-3 — Validator now accepts DSers' single-SKU <none> sentinel. DSers natively stores supplyVariantId: "<none>" as a literal string for Default Title single-variant products. Before this fix the validator's \d+:\d+[#...] pattern was wrongly rejecting this real production data, which meant every single-SKU product in the account would fail on apply.
O-R55-2 / B-R57-1 — sku-matcher single-SKU seller shortcut. Fixes a matcher blind spot where a seller is Default Title (1v) and the candidate is 1v with a real dimension name like Color. The new shortcut fires only when the seller is trivially single-SKU (isTrivialSellerSingleSku) and produces confidence 75 with reason single_sku_seller_trivial. The existing algorithm core is untouched — the shortcut is strictly an early-return at the top of matchVariants, preserving all multi-SKU paths and the fat-fat 1v↔1v exact-match path (which still scores 80).
File hygiene
Fixed a hardcoded version: "1.3.8" in app/dropshipping/[transport]/route.ts — a legacy drop that had survived three releases without being noticed.
package.json / server.json / manifest.json / README all synced to 1.5.0.
Added CHANGELOG.md.
npm package only ships dist/ + README + LICENSE + CHANGELOG — 239 KB tarball. src/ / app/ / test/ / docs/ / scripts/ are not in the published package.
Known limitations (deferred to later versions)
dsers_my_productspage pagination doesn't actually advance (DSers backend behavior, not a tool bug). Workaround: use page_size:100 to pull everything at once. Accounts with >100 products will hit this; tracking separately.
DSers GET mapping response returns a non-deterministic mappings[] array order for multi-supplier (MapAgc) products. Values are identical, only the index shifts. Affects byte-level diff workflows; the validator doesn't depend on byte-level comparison so it's not impacted.
dsers_sku_remap discover mode (Path B) doesn't work on vp* virtual products — DSers-side limitation, no seed images available to reverse-search.
Regression validation
Every round from round-1 to round-5.7.5 was run by an independent QA session with no access to prior reports, HANDOFF docs, commit bodies, or test files. Full e2e regression + real DSers writes + byte-level rollback verification. The last round: 0 Blocker / 0 Major / 0 Minor / 0 Observation. 585/585 vitest tests green.
1.4.x and earlier
Previous releases live in the git history. Mostly OAuth 2.1 + remote MCP + Vercel deployment work. No consolidated changelog; check git log --oneline v1.4.0..v1.5.0 or individual commit messages.