fix(stalker): drop the unanchored auth-failure sweep in the session service

Anchoring the body detector closed one door and left another open. The session
service still stringified the whole response and matched an UNANCHORED
`authorization failed`, so a short page from something in FRONT of the portal
retired the token, retried, and threw `Authorization failed after retry` —
whose own message then matched the repair trigger's wide phrase set and
re-probed a portal that had refused nothing.

The shared detector already covers every real shape, including the
`js.error`/`js.msg` envelopes the sweep was also catching, so removing it
costs no coverage. Regression goes through `makeAuthenticatedRequest` rather
than the primitive, since that is where the chain actually ran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-03 00:03:55 +02:00
1 parent 6cb195197d
commit 59d4060afb
2 files changed
+33 -24

No files matched your search

@@ -259,6 +259,28 @@ describe('StalkerSessionService identity-tagged token cache', () => {
}
);
it('does not treat a proxy page mentioning the phrase as an auth failure', async () => {
// End to end through makeAuthenticatedRequest, not just the
// primitive: this is the path that used to stringify the whole
// response and match an unanchored `authorization failed`. A short
// page from something in FRONT of the portal would retire the token,
// retry, and throw a message that itself matches the repair trigger —
// re-probing a portal that never refused anything.
service.setCachedToken('portal-1', 'LIVE', playlistA);
const body = 'The request Authorization failed. Try again later.';
sendIpcEvent.mockResolvedValue(body);
await expect(
service.makeAuthenticatedRequest(playlistA, {
action: 'get_genres',
})
).resolves.toBe(body);
// No retry, no retirement, nothing for the repair layer to act on.
expect(sendIpcEvent).toHaveBeenCalledTimes(1);
expect(service.getCachedToken('portal-1')).toBe('LIVE');
});
it('retires a failed token even on the no-retry path (watchdog pings)', async () => {
service.setCachedToken('portal-1', 'DEAD', playlistA);
sendIpcEvent.mockResolvedValue('Authorization failed.');
@@ -801,30 +801,17 @@ export class StalkerSessionService {
return true;
}
// Convert response to string for pattern matching
const responseStr = JSON.stringify(responseOrError).toLowerCase();
// Check for "Authorization failed. XX" pattern (like "Authorization failed. 75")
if (/authorization\s*failed\.?\s*\d*/i.test(responseStr)) {
return true;
}
// Check for common auth failure indicators
const jsData = response?.['js'] as Record<string, unknown>;
const errorMessage =
(response?.['message'] as string)?.toLowerCase?.() ||
jsData?.['error']?.toString?.().toLowerCase?.() ||
jsData?.['msg']?.toString?.().toLowerCase?.() ||
'';
return (
errorMessage.includes('authorization') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('auth failed') ||
errorMessage.includes('invalid token') ||
response?.['status'] === 401 ||
jsData?.['error'] === 'Authorization failed'
);
// Everything above is structural. What used to follow was a
// `JSON.stringify(responseOrError)` sweep with an UNANCHORED
// `authorization failed` pattern — the same false positive the body
// detector was just anchored against, reachable through a different
// door: a short proxy/WAF page containing the phrase retired the
// token, retried, and threw `Authorization failed after retry`,
// whose own message then matched the repair trigger and re-probed a
// portal that never refused anything. The shared detector already
// covers every real shape, including the `js.error`/`js.msg`
// envelopes, so the sweep only added the false positive.
return response?.['status'] === 401 || response?.['status'] === 403;
}
/**