mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(stalker): retire only the token that actually failed auth
A request dispatched with the previous token can see its authorization failure arrive after a profile refresh has already cached a fresh one. The retry path deleted the cache blindly, killing the fresh token and kicking off another handshake that in turn invalidated tokens of newer requests — cascading retries on strict portals. makeAuthenticatedRequest() now retires the cached token only while it still equals the token that failed; a late failure of a stale token leaves the refreshed token in place and the retry reuses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
79c643b42b
commit
7bc14d3693
2 files changed
+79
-4
No files matched your search
@@ -283,6 +283,65 @@ describe('StalkerSessionService identity payloads', () => {
|
||||
expect(service.getCachedToken(playlist._id)).toBe('fresh-token');
|
||||
});
|
||||
|
||||
it('keeps a freshly refreshed token when a stale request fails auth late', async () => {
|
||||
const playlist = {
|
||||
_id: 'playlist-5',
|
||||
portalUrl,
|
||||
macAddress,
|
||||
isFullStalkerPortal: true,
|
||||
} as Playlist;
|
||||
|
||||
// The request went out with the previous token; meanwhile a
|
||||
// profile refresh has already cached a fresh one.
|
||||
jest.spyOn(service, 'ensureToken')
|
||||
.mockResolvedValueOnce({ token: 'stale-token' })
|
||||
.mockResolvedValueOnce({ token: 'fresh-token' });
|
||||
service.setCachedToken(playlist._id, 'fresh-token');
|
||||
|
||||
dataService.sendIpcEvent
|
||||
.mockResolvedValueOnce({ js: 'Authorization failed. 75' })
|
||||
.mockResolvedValueOnce({ js: { data: [] } });
|
||||
|
||||
await service.makeAuthenticatedRequest(playlist, {
|
||||
type: 'itv',
|
||||
action: 'get_ordered_list',
|
||||
});
|
||||
|
||||
// The late failure of the stale token must not delete the fresh
|
||||
// one — the retry reuses it instead of forcing a new handshake.
|
||||
expect(service.getCachedToken(playlist._id)).toBe('fresh-token');
|
||||
expect(dataService.sendIpcEvent).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ token: 'fresh-token' })
|
||||
);
|
||||
});
|
||||
|
||||
it('still retires the cached token when it is the one that failed', async () => {
|
||||
const playlist = {
|
||||
_id: 'playlist-6',
|
||||
portalUrl,
|
||||
macAddress,
|
||||
isFullStalkerPortal: true,
|
||||
} as Playlist;
|
||||
|
||||
jest.spyOn(service, 'ensureToken')
|
||||
.mockResolvedValueOnce({ token: 'dead-token' })
|
||||
.mockResolvedValueOnce({ token: 'new-token' });
|
||||
service.setCachedToken(playlist._id, 'dead-token');
|
||||
|
||||
dataService.sendIpcEvent
|
||||
.mockResolvedValueOnce({ js: 'Authorization failed. 75' })
|
||||
.mockResolvedValueOnce({ js: { data: [] } });
|
||||
|
||||
await service.makeAuthenticatedRequest(playlist, {
|
||||
type: 'itv',
|
||||
action: 'get_ordered_list',
|
||||
});
|
||||
|
||||
// Existing behavior preserved: the failed token itself is gone.
|
||||
expect(service.getCachedToken(playlist._id)).toBeNull();
|
||||
});
|
||||
|
||||
it('refreshes the account profile even when a pending authentication fails', async () => {
|
||||
const playlist = {
|
||||
_id: 'playlist-2',
|
||||
|
||||
@@ -606,6 +606,22 @@ export class StalkerSessionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cached token only while it is still the one that just
|
||||
* failed. A request dispatched with the previous token can see its
|
||||
* authorization failure arrive after a profile refresh has already
|
||||
* cached a fresh token — deleting blindly would kill that fresh token
|
||||
* and trigger a cascade of competing handshakes on strict portals.
|
||||
*/
|
||||
private retireFailedToken(
|
||||
playlistId: string,
|
||||
failedToken: string | null
|
||||
): void {
|
||||
if (failedToken && this.getCachedToken(playlistId) === failedToken) {
|
||||
this.clearCachedToken(playlistId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a response or error indicates an authorization failure
|
||||
*/
|
||||
@@ -667,8 +683,8 @@ export class StalkerSessionService {
|
||||
// Check for authorization failure in response
|
||||
if (this.isAuthorizationError(response)) {
|
||||
if (retryOnAuthFailure && playlist.isFullStalkerPortal) {
|
||||
// Clear cached token to force re-authentication
|
||||
this.clearCachedToken(playlist._id);
|
||||
// Retire the failed token so the retry re-authenticates
|
||||
this.retireFailedToken(playlist._id, token);
|
||||
// Retry once with fresh authentication
|
||||
return this.makeAuthenticatedRequest<T>(
|
||||
playlist,
|
||||
@@ -688,8 +704,8 @@ export class StalkerSessionService {
|
||||
retryOnAuthFailure &&
|
||||
playlist.isFullStalkerPortal
|
||||
) {
|
||||
// Clear cached token and retry with new handshake
|
||||
this.clearCachedToken(playlist._id);
|
||||
// Retire the failed token and retry with new handshake
|
||||
this.retireFailedToken(playlist._id, token);
|
||||
return this.makeAuthenticatedRequest<T>(
|
||||
playlist,
|
||||
params,
|
||||
|
||||
Reference in new issue
Block a user