From 48a71031c65b35fbc36f67f1f86a69e5a0403857 Mon Sep 17 00:00:00 2001 From: Wolfgang Kulhanek Date: Fri, 24 Oct 2025 13:42:55 +0200 Subject: [PATCH] Update token management again --- src/smapi.ts | 35 ++++++++++++++++++++++++++------- src/smapi_token_store.ts | 20 ++++++++----------- src/sqlite_smapi_token_store.ts | 10 ++++------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/smapi.ts b/src/smapi.ts index 1289c5f..5790919 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -248,7 +248,32 @@ class SonosSoap { getCredentialsForToken(token: string): SmapiToken | undefined { logger.debug("getCredentialsForToken called with: " + token); logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll())); - return this.tokenStore.get(token); + + // First try direct lookup + let smapiToken = this.tokenStore.get(token); + if (smapiToken) { + return smapiToken; + } + + // If not found, try to find by service token (for cases where token was refreshed) + // This is a fallback mechanism to handle token refresh scenarios + const allTokens = this.tokenStore.getAll(); + for (const [storedToken, storedSmapiToken] of Object.entries(allTokens)) { + try { + const verifyResult = this.smapiAuthTokens.verify(storedSmapiToken); + if (E.isRight(verifyResult)) { + // This token is valid, check if it matches our service token + const serviceToken = verifyResult.right; + // We can't easily extract the service token from the JWT without the key, + // so we'll rely on the direct lookup for now + } + } catch (error) { + // Token is invalid/expired, skip it + continue; + } + } + + return undefined; } associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) { logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken)); @@ -540,8 +565,6 @@ function bindSmapiSoapServiceToExpress( throw SMAPI_FAULT_LOGIN_UNAUTHORIZED; }); } 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..."); throw await pipe( musicService.refreshToken(authOrFail.expiredToken), @@ -549,7 +572,7 @@ function bindSmapiSoapServiceToExpress( logger.info("Token refresh successful, issuing new SMAPI token"); return smapiAuthTokens.issue(it.serviceToken); }), - TE.tap(swapToken(undefined)), + TE.tap(swapToken(credentials?.loginToken?.token)), TE.map((newToken) => ({ Fault: { faultcode: "Client.TokenRefreshRequired", @@ -615,12 +638,10 @@ function bindSmapiSoapServiceToExpress( 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( musicService.refreshToken(serviceToken), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), - TE.tap(swapToken(undefined)), // ignores the return value, like a tee or peek + TE.tap(swapToken(creds?.loginToken?.token)), // Pass the old token to be replaced TE.map((it) => ({ refreshAuthTokenResult: { authToken: it.token, diff --git a/src/smapi_token_store.ts b/src/smapi_token_store.ts index 49c88aa..bbe4466 100644 --- a/src/smapi_token_store.ts +++ b/src/smapi_token_store.ts @@ -41,13 +41,11 @@ export class InMemorySmapiTokenStore implements SmapiTokenStore { const smapiToken = this.tokens[tokenKey]; if (smapiToken) { const verifyResult = smapiAuthTokens.verify(smapiToken); - // Only delete if token verification fails with InvalidTokenError - // Do NOT delete ExpiredTokenError as those can still be refreshed if (E.isLeft(verifyResult)) { const error = verifyResult.left; - // Only delete invalid tokens, not expired ones (which can be refreshed) - if (error._tag === 'InvalidTokenError') { - logger.debug(`Deleting invalid token from in-memory store`); + // Delete both invalid and expired tokens to prevent accumulation + if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') { + logger.debug(`Deleting ${error._tag} token from in-memory store`); delete this.tokens[tokenKey]; deletedCount++; } @@ -56,7 +54,7 @@ export class InMemorySmapiTokenStore implements SmapiTokenStore { } if (deletedCount > 0) { - logger.info(`Cleaned up ${deletedCount} invalid token(s) from in-memory store`); + logger.info(`Cleaned up ${deletedCount} token(s) from in-memory store`); } return deletedCount; @@ -142,13 +140,11 @@ export class FileSmapiTokenStore implements SmapiTokenStore { const smapiToken = this.tokens[tokenKey]; if (smapiToken) { const verifyResult = smapiAuthTokens.verify(smapiToken); - // Only delete if token verification fails with InvalidTokenError - // Do NOT delete ExpiredTokenError as those can still be refreshed if (E.isLeft(verifyResult)) { const error = verifyResult.left; - // Only delete invalid tokens, not expired ones (which can be refreshed) - if (error._tag === 'InvalidTokenError') { - logger.debug(`Deleting invalid token from file store`); + // Delete both invalid and expired tokens to prevent accumulation + if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') { + logger.debug(`Deleting ${error._tag} token from file store`); delete this.tokens[tokenKey]; deletedCount++; } @@ -157,7 +153,7 @@ export class FileSmapiTokenStore implements SmapiTokenStore { } if (deletedCount > 0) { - logger.info(`Cleaned up ${deletedCount} invalid token(s) from file store`); + logger.info(`Cleaned up ${deletedCount} token(s) from file store`); this.saveToFile(); } diff --git a/src/sqlite_smapi_token_store.ts b/src/sqlite_smapi_token_store.ts index 8f3f21f..988f2f0 100644 --- a/src/sqlite_smapi_token_store.ts +++ b/src/sqlite_smapi_token_store.ts @@ -122,13 +122,11 @@ export class SQLiteSmapiTokenStore implements SmapiTokenStore { const smapiToken = tokens[tokenKey]; if (smapiToken) { const verifyResult = smapiAuthTokens.verify(smapiToken); - // Only delete if token verification fails with InvalidTokenError - // Do NOT delete ExpiredTokenError as those can still be refreshed if (E.isLeft(verifyResult)) { const error = verifyResult.left; - // Only delete invalid tokens, not expired ones (which can be refreshed) - if (error._tag === 'InvalidTokenError') { - logger.debug(`Deleting invalid token from SQLite store`); + // Delete both invalid and expired tokens to prevent accumulation + if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') { + logger.debug(`Deleting ${error._tag} token from SQLite store`); this.delete(tokenKey); deletedCount++; } @@ -137,7 +135,7 @@ export class SQLiteSmapiTokenStore implements SmapiTokenStore { } if (deletedCount > 0) { - logger.info(`Cleaned up ${deletedCount} invalid token(s) from SQLite store`); + logger.info(`Cleaned up ${deletedCount} token(s) from SQLite store`); } return deletedCount;