Treat Aurora API responses as untrusted input with runtime validation:
- Fix P0: draft_climbs can no longer overwrite published climbs. All fields
in onConflictDoUpdate are gated on isDraft=true via SQL CASE expressions.
- Fix P0: isDraft transition corrected to only allow true->false (publishing),
never false->true (unpublishing). Previous code had this backwards.
- Fix P1: climb_stats numeric fields validated with sanitizeNumber() and
clamped to reasonable bounds. Invalid values preserve existing DB values.
- Fix P1: Sync timestamps validated - rejects unparseable/pre-2016, clamps
future timestamps. table_name validated against known allowlists.
- Fix P2: Per-table record cap of 10,000 prevents memory exhaustion.
- Fix P2: Beta link URLs validated as http/https, invalid thumbnails nulled.
- Fix P2: String fields truncated to reasonable limits across all sync paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
draft_climbscan no longer overwrite published climbs. Both user-sync files now gate allonConflictDoUpdatefields onisDraft = trueusing SQL CASE expressions. Published climbs are fully immutable via sync.isDraftCASE inshared-sync.ts— only allowstrue → false(publishing), neverfalse → true(unpublishing).climb_statsnumeric fields validated withsanitizeNumber()and clamped to bounds (difficulty 0-50, quality 0-5, ascensionistCount 0-10M). Invalid incoming values preserve existing DB values on conflict. History inserts skipped when all values invalid.table_namevalidated against known allowlists in bothupdateSharedSyncsandupdateUserSyncs.New
sync-validation.tsutility files created in both packages (duplicated to match existing pattern; consolidation is a follow-up).Test plan
npm run typecheckpasses (verified)npm run lintpasses with 0 errors (verified)draft_climbsmatch the proven pattern fromshared-sync.tsupsertClimbs🤖 Generated with Claude Code