@@ -905,6 +905,31 @@ export const ConnectionScreen: React.FC<ConnectionScreenProps> = ({
905905 return `${ pid } |${ proto } ` ;
906906 } ;
907907
908+ // Legacy persisted snapshots (pre collision-free keys) used
909+ // `providerId || protocol` as the map key. Re-key them to the current
910+ // `pid|proto` form at hydrate time, so per-mode credentials saved before
911+ // the rekey keep restoring instead of coming up blank, and the old-format
912+ // entries do not linger in the vault map as dead secrets. For the one
913+ // historic collision (Koofr: both modes hashed to 'koofr') the surviving
914+ // entry deterministically lands on the group's first matching mode.
915+ const migrateLegacyModeKeys = (
916+ persisted : ModeCredentialMap ,
917+ group : ReturnType < typeof findActiveModeGroup >
918+ ) : ModeCredentialMap => {
919+ if ( ! group ) return persisted ;
920+ const out : ModeCredentialMap = { ...persisted } ;
921+ for ( const mode of group . modes ) {
922+ const proto = mode . protocol as string ;
923+ const legacyKey = mode . providerId || proto ;
924+ const newKey = modeStashKey ( mode . providerId , proto ) ;
925+ if ( legacyKey !== newKey && out [ legacyKey ] && ! out [ newKey ] ) {
926+ out [ newKey ] = out [ legacyKey ] ;
927+ delete out [ legacyKey ] ;
928+ }
929+ }
930+ return out ;
931+ } ;
932+
908933 // The stash key of the currently-active mode, matching the oldKey/newKey
909934 // convention in handleProtocolChange.
910935 const computeActiveModeKey = ( ) : string => {
@@ -1895,7 +1920,10 @@ export const ConnectionScreen: React.FC<ConnectionScreenProps> = ({
18951920 try {
18961921 const persisted = await loadModeCredentials ( targetProfileId ) ;
18971922 if ( editingProfileIdRef . current === targetProfileId ) {
1898- modeCredentialSnapshotsRef . current = persisted ;
1923+ // Re-key any legacy-format snapshot so pre-rekey saves
1924+ // still restore on the first mode switch.
1925+ const group = findActiveModeGroup ( profile . providerId , profile . protocol ) ;
1926+ modeCredentialSnapshotsRef . current = migrateLegacyModeKeys ( persisted , group ) ;
18991927 }
19001928 } catch {
19011929 // No persisted modes: in-session snapshots only.
@@ -2336,9 +2364,12 @@ export const ConnectionScreen: React.FC<ConnectionScreenProps> = ({
23362364
23372365 // #369: the 2FA code fields (Filen / MEGA / Internxt) accept digits only,
23382366 // so a stray letter or space can never make a TOTP silently wrong. Shared
2339- // by all three forms so the behaviour stays identical.
2367+ // by all three forms so the behaviour stays identical. The 6-digit clamp
2368+ // lives HERE, not in a maxLength attribute: the DOM truncates a paste
2369+ // BEFORE the change event, so "123 456" under maxLength arrived as
2370+ // "123 45" and stripped to five digits, silently corrupting the code.
23402371 const handleTotpCodeChange = ( raw : string ) => {
2341- const digits = raw . replace ( / \D / g, '' ) ;
2372+ const digits = raw . replace ( / \D / g, '' ) . slice ( 0 , 6 ) ;
23422373 onConnectionParamsChange ( {
23432374 ...connectionParams ,
23442375 options : { ...connectionParams . options , two_factor_code : digits || undefined } ,
@@ -4566,7 +4597,6 @@ export const ConnectionScreen: React.FC<ConnectionScreenProps> = ({
45664597 onChange = { ( e ) => handleTotpCodeChange ( e . target . value ) }
45674598 className = "w-32 px-4 py-2.5 text-center tracking-[0.3em] font-mono bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
45684599 placeholder = "000000"
4569- maxLength = { 6 }
45704600 inputMode = "numeric"
45714601 autoComplete = "one-time-code"
45724602 />
@@ -4732,7 +4762,6 @@ export const ConnectionScreen: React.FC<ConnectionScreenProps> = ({
47324762 onChange = { ( e ) => handleTotpCodeChange ( e . target . value ) }
47334763 className = "w-32 px-4 py-2.5 text-center tracking-[0.3em] font-mono bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
47344764 placeholder = "000000"
4735- maxLength = { 6 }
47364765 inputMode = "numeric"
47374766 autoComplete = "one-time-code"
47384767 />
@@ -5149,7 +5178,6 @@ export const ConnectionScreen: React.FC<ConnectionScreenProps> = ({
51495178 onChange = { ( e ) => handleTotpCodeChange ( e . target . value ) }
51505179 className = "w-32 px-4 py-2.5 text-center tracking-[0.3em] font-mono bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-red-500 focus:border-red-500"
51515180 placeholder = "000000"
5152- maxLength = { 6 }
51535181 inputMode = "numeric"
51545182 autoComplete = "one-time-code"
51555183 />
0 commit comments