Compare commits

...

4 Commits

Author SHA1 Message Date
Wolfgang Kulhanek
2403d6cdc6 Another token expiration fix. 2025-10-24 15:07:09 +02:00
Wolfgang Kulhanek
03434fb362 More token refresh fixes 2025-10-24 14:56:56 +02:00
Wolfgang Kulhanek
a47581c3fe Fix tests 2025-10-24 14:47:53 +02:00
Wolfgang Kulhanek
48a71031c6 Update token management again 2025-10-24 13:42:55 +02:00
3 changed files with 17 additions and 24 deletions

View File

@@ -250,11 +250,10 @@ class SonosSoap {
logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll())); logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll()));
return this.tokenStore.get(token); return this.tokenStore.get(token);
} }
associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) { associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken) {
logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken)); logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken));
if(oldToken) { // Don't immediately delete old token to avoid race conditions
this.tokenStore.delete(oldToken); // The cleanup process will handle expired tokens later
}
this.tokenStore.set(token, fullSmapiToken); this.tokenStore.set(token, fullSmapiToken);
} }
} }
@@ -488,11 +487,9 @@ function bindSmapiSoapServiceToExpress(
const swapToken = (expiredToken: string | undefined) => (newToken: SmapiToken) => { const swapToken = (expiredToken: string | undefined) => (newToken: SmapiToken) => {
logger.debug("oldToken: " + expiredToken); logger.debug("oldToken: " + expiredToken);
logger.debug("newToken: " + JSON.stringify(newToken)); logger.debug("newToken: " + JSON.stringify(newToken));
if (expiredToken) { // Always add the new token, but don't immediately delete the old one
sonosSoap.associateCredentialsForToken(newToken.token, newToken, expiredToken); // to avoid race conditions where Sonos might still be using the old token
} else {
sonosSoap.associateCredentialsForToken(newToken.token, newToken); sonosSoap.associateCredentialsForToken(newToken.token, newToken);
}
return TE.right(newToken); return TE.right(newToken);
} }
@@ -540,8 +537,6 @@ function bindSmapiSoapServiceToExpress(
throw SMAPI_FAULT_LOGIN_UNAUTHORIZED; throw SMAPI_FAULT_LOGIN_UNAUTHORIZED;
}); });
} else if (isExpiredTokenError(authOrFail)) { } else if (isExpiredTokenError(authOrFail)) {
// Don't pass old token here to avoid circular reference issues with Jest/SOAP
// Old expired tokens will be cleaned up by TTL or manual cleanup later
logger.info("Token expired, attempting refresh..."); logger.info("Token expired, attempting refresh...");
throw await pipe( throw await pipe(
musicService.refreshToken(authOrFail.expiredToken), musicService.refreshToken(authOrFail.expiredToken),
@@ -549,7 +544,7 @@ function bindSmapiSoapServiceToExpress(
logger.info("Token refresh successful, issuing new SMAPI token"); logger.info("Token refresh successful, issuing new SMAPI token");
return smapiAuthTokens.issue(it.serviceToken); return smapiAuthTokens.issue(it.serviceToken);
}), }),
TE.tap(swapToken(undefined)), TE.tap(swapToken(authOrFail.expiredToken)), // Pass the expired token to ensure it gets deleted
TE.map((newToken) => ({ TE.map((newToken) => ({
Fault: { Fault: {
faultcode: "Client.TokenRefreshRequired", faultcode: "Client.TokenRefreshRequired",
@@ -615,12 +610,10 @@ function bindSmapiSoapServiceToExpress(
throw fault.toSmapiFault(); throw fault.toSmapiFault();
}) })
); );
// Don't pass old token here to avoid circular reference issues with Jest/SOAP
// Old expired tokens will be cleaned up by TTL or manual cleanup later
return pipe( return pipe(
musicService.refreshToken(serviceToken), musicService.refreshToken(serviceToken),
TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)),
TE.tap(swapToken(undefined)), // ignores the return value, like a tee or peek TE.tap(swapToken(serviceToken)), // Pass the expired token to ensure it gets deleted
TE.map((it) => ({ TE.map((it) => ({
refreshAuthTokenResult: { refreshAuthTokenResult: {
authToken: it.token, authToken: it.token,

View File

@@ -45,9 +45,9 @@ export class InMemorySmapiTokenStore implements SmapiTokenStore {
// Do NOT delete ExpiredTokenError as those can still be refreshed // Do NOT delete ExpiredTokenError as those can still be refreshed
if (E.isLeft(verifyResult)) { if (E.isLeft(verifyResult)) {
const error = verifyResult.left; const error = verifyResult.left;
// Only delete invalid tokens, not expired ones (which can be refreshed) // Delete both invalid and expired tokens to prevent accumulation
if (error._tag === 'InvalidTokenError') { if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
logger.debug(`Deleting invalid token from in-memory store`); logger.debug(`Deleting ${error._tag} token from in-memory store`);
delete this.tokens[tokenKey]; delete this.tokens[tokenKey];
deletedCount++; deletedCount++;
} }
@@ -146,9 +146,9 @@ export class FileSmapiTokenStore implements SmapiTokenStore {
// Do NOT delete ExpiredTokenError as those can still be refreshed // Do NOT delete ExpiredTokenError as those can still be refreshed
if (E.isLeft(verifyResult)) { if (E.isLeft(verifyResult)) {
const error = verifyResult.left; const error = verifyResult.left;
// Only delete invalid tokens, not expired ones (which can be refreshed) // Delete both invalid and expired tokens to prevent accumulation
if (error._tag === 'InvalidTokenError') { if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
logger.debug(`Deleting invalid token from file store`); logger.debug(`Deleting ${error._tag} token from file store`);
delete this.tokens[tokenKey]; delete this.tokens[tokenKey];
deletedCount++; deletedCount++;
} }

View File

@@ -126,9 +126,9 @@ export class SQLiteSmapiTokenStore implements SmapiTokenStore {
// Do NOT delete ExpiredTokenError as those can still be refreshed // Do NOT delete ExpiredTokenError as those can still be refreshed
if (E.isLeft(verifyResult)) { if (E.isLeft(verifyResult)) {
const error = verifyResult.left; const error = verifyResult.left;
// Only delete invalid tokens, not expired ones (which can be refreshed) // Delete both invalid and expired tokens to prevent accumulation
if (error._tag === 'InvalidTokenError') { if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
logger.debug(`Deleting invalid token from SQLite store`); logger.debug(`Deleting ${error._tag} token from SQLite store`);
this.delete(tokenKey); this.delete(tokenKey);
deletedCount++; deletedCount++;
} }