mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
feat(stalker): protocol-correct auth lifecycle (#1354)
This commit is contained in:
1 parent
e197409b10
commit
d2a83164ec
56 files changed
+4807
-709
No files matched your search
@@ -441,6 +441,115 @@ describe('PlaylistsService', () => {
|
||||
).toBe('1');
|
||||
});
|
||||
|
||||
it('carries the Stalker session fields through the SQLite fallback read', async () => {
|
||||
// Without these, every cold Electron session loses the persisted
|
||||
// cadence AND the identity the token was negotiated for — so the
|
||||
// mismatch check cannot run and the watchdog falls back to default.
|
||||
const electron = {
|
||||
dbGetAppPlaylist: jest.fn(async () => ({
|
||||
id: 'stalker-1',
|
||||
stalkerToken: 'TOKEN',
|
||||
stalkerSessionIdentity: 'fingerprint-1',
|
||||
stalkerWatchdogTimeout: 90,
|
||||
stalkerTimeslot: 7,
|
||||
})),
|
||||
dbGetAppPlaylists: jest.fn(async () => []),
|
||||
dbGetAppState: jest.fn(async () => '1'),
|
||||
dbSetAppState: jest.fn(),
|
||||
dbUpsertAppPlaylist: jest.fn(),
|
||||
dbUpsertAppPlaylists: jest.fn(),
|
||||
};
|
||||
testWindow.electron = electron;
|
||||
|
||||
const service = createService();
|
||||
const playlist = await firstValueFrom(
|
||||
service.getPlaylistById('stalker-1')
|
||||
);
|
||||
|
||||
expect(playlist).toEqual(
|
||||
expect.objectContaining({
|
||||
stalkerToken: 'TOKEN',
|
||||
stalkerSessionIdentity: 'fingerprint-1',
|
||||
stalkerWatchdogTimeout: 90,
|
||||
stalkerTimeslot: 7,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('clears a stale watchdog cadence when the portal stops advertising one', async () => {
|
||||
// Reusing the token skips the profile that would correct the cadence,
|
||||
// so leaving the old value on the row means the next restart applies
|
||||
// a cadence the portal no longer asks for.
|
||||
const existingPlaylist: Playlist = {
|
||||
_id: 'stalker-1',
|
||||
title: 'Stalker',
|
||||
count: 0,
|
||||
importDate: new Date('2026-08-02T00:00:00.000Z').toISOString(),
|
||||
lastUsage: new Date('2026-08-02T00:00:00.000Z').toISOString(),
|
||||
autoRefresh: false,
|
||||
stalkerToken: 'OLD',
|
||||
stalkerWatchdogTimeout: 45,
|
||||
stalkerTimeslot: 7,
|
||||
} as Playlist;
|
||||
const dbService = {
|
||||
getAll: jest.fn(() => of([])),
|
||||
getByID: jest.fn(() => of(existingPlaylist)),
|
||||
update: jest.fn((_storeName: string, playlist: Playlist) =>
|
||||
of(playlist)
|
||||
),
|
||||
};
|
||||
testWindow.electron = undefined;
|
||||
|
||||
const service = createService(dbService);
|
||||
|
||||
await firstValueFrom(
|
||||
service.updateStalkerSession('stalker-1', { stalkerToken: 'NEW' })
|
||||
);
|
||||
|
||||
const written = dbService.update.mock.calls[0][1] as Playlist;
|
||||
expect(written.stalkerToken).toBe('NEW');
|
||||
expect(written.stalkerWatchdogTimeout).toBeUndefined();
|
||||
expect(written.stalkerTimeslot).toBeUndefined();
|
||||
});
|
||||
|
||||
it('persists the watchdog cadence alongside the stalker token', async () => {
|
||||
const existingPlaylist: Playlist = {
|
||||
_id: 'stalker-1',
|
||||
title: 'Stalker',
|
||||
count: 0,
|
||||
importDate: new Date('2026-08-02T00:00:00.000Z').toISOString(),
|
||||
lastUsage: new Date('2026-08-02T00:00:00.000Z').toISOString(),
|
||||
autoRefresh: false,
|
||||
} as Playlist;
|
||||
const dbService = {
|
||||
getAll: jest.fn(() => of([])),
|
||||
getByID: jest.fn(() => of(existingPlaylist)),
|
||||
update: jest.fn((_storeName: string, playlist: Playlist) =>
|
||||
of(playlist)
|
||||
),
|
||||
};
|
||||
testWindow.electron = undefined;
|
||||
|
||||
const service = createService(dbService);
|
||||
|
||||
await firstValueFrom(
|
||||
service.updateStalkerSession('stalker-1', {
|
||||
stalkerToken: 'TOKEN',
|
||||
stalkerWatchdogTimeout: 90,
|
||||
stalkerTimeslot: 12,
|
||||
})
|
||||
);
|
||||
|
||||
expect(dbService.update).toHaveBeenCalledWith(
|
||||
DbStores.Playlists,
|
||||
expect.objectContaining({
|
||||
stalkerToken: 'TOKEN',
|
||||
stalkerWatchdogTimeout: 90,
|
||||
stalkerTimeslot: 12,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('persists hiddenGroupTitles in playlist meta updates', async () => {
|
||||
const existingPlaylist: Playlist = {
|
||||
_id: 'playlist-1',
|
||||
|
||||
@@ -242,6 +242,13 @@ export class PlaylistsService {
|
||||
stalkerSignature2: playlist.stalkerSignature2,
|
||||
isFullStalkerPortal: playlist.isFullStalkerPortal,
|
||||
stalkerToken: playlist.stalkerToken,
|
||||
// Without these the Electron cold read loses the persisted
|
||||
// cadence AND the identity the token was negotiated for, so the
|
||||
// mismatch check cannot run and the watchdog falls back to the
|
||||
// default.
|
||||
stalkerSessionIdentity: playlist.stalkerSessionIdentity,
|
||||
stalkerWatchdogTimeout: playlist.stalkerWatchdogTimeout,
|
||||
stalkerTimeslot: playlist.stalkerTimeslot,
|
||||
stalkerAccountInfo: playlist.stalkerAccountInfo,
|
||||
} as Playlist;
|
||||
}
|
||||
@@ -729,6 +736,45 @@ export class PlaylistsService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a freshly negotiated Stalker session on the playlist so the
|
||||
* next app start can re-present the token (the portal handshake is
|
||||
* idempotent) and keep the portal's own watchdog cadence — reusing a
|
||||
* token skips the `get_profile` that carries the cadence, so it has to
|
||||
* survive with the token. No-ops when the playlist row does not exist
|
||||
* yet: the import flow saves both with the playlist itself.
|
||||
*/
|
||||
updateStalkerSession(
|
||||
playlistId: string,
|
||||
session: {
|
||||
stalkerToken: string;
|
||||
stalkerSessionIdentity?: string;
|
||||
stalkerWatchdogTimeout?: number;
|
||||
stalkerTimeslot?: number;
|
||||
}
|
||||
) {
|
||||
return this.serializePlaylistWrite(playlistId, async () => {
|
||||
const playlist = await firstValueFrom(
|
||||
this.getPlaylistById(playlistId)
|
||||
);
|
||||
if (!playlist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The cadence is written unconditionally, including as
|
||||
// `undefined`: a portal that stops advertising one must not leave
|
||||
// a stale value behind for the next restart to re-apply, since
|
||||
// reusing the token skips the profile that would correct it.
|
||||
return this.persistPlaylistMutation({
|
||||
...playlist,
|
||||
stalkerToken: session.stalkerToken,
|
||||
stalkerSessionIdentity: session.stalkerSessionIdentity,
|
||||
stalkerWatchdogTimeout: session.stalkerWatchdogTimeout,
|
||||
stalkerTimeslot: session.stalkerTimeslot,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateFavorites(id: string, favorites: string[]) {
|
||||
return this.serializePlaylistWrite(id, async () => {
|
||||
const playlist = await firstValueFrom(this.getPlaylistById(id));
|
||||
|
||||
Reference in new issue
Block a user