From 8f7c4761ea05c4229ae573772796cfdf74704d06 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Jul 2026 14:00:51 +0200 Subject: [PATCH] fix(backup): invalidate restored stalker state --- .../src/backup-security.e2e.ts | 36 ++++++- .../src/support/backup-security.ts | 4 + ...ckup.service.stalker-learned-state.spec.ts | 95 +++++++++++++++++++ .../src/lib/playlist-backup.service.ts | 4 + 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 libs/services/src/lib/playlist-backup.service.stalker-learned-state.spec.ts diff --git a/apps/electron-backend-e2e/src/backup-security.e2e.ts b/apps/electron-backend-e2e/src/backup-security.e2e.ts index 84d1c53ca..f13b32e6c 100644 --- a/apps/electron-backend-e2e/src/backup-security.e2e.ts +++ b/apps/electron-backend-e2e/src/backup-security.e2e.ts @@ -212,6 +212,14 @@ test.describe('Electron playlist backup security acceptance', () => { }, }, skippedXtream, + { + ...redactedStalker, + connection: { ...redactedStalker.connection }, + userState: { + favorites: [], + recentlyViewed: [], + }, + }, ], }; writeFileSync( @@ -278,7 +286,7 @@ test.describe('Electron playlist backup security acceptance', () => { await expect( app.mainWindow.getByText( - 'Backup import finished: 0 imported, 1 merged, 1 skipped, 0 failed.' + 'Backup import finished: 0 imported, 2 merged, 1 skipped, 0 failed.' ) ).toBeVisible({ timeout: 15000 }); @@ -304,6 +312,32 @@ test.describe('Electron playlist backup security acceptance', () => { await readPlaylist(app.mainWindow, SKIPPED_XTREAM_PLAYLIST_ID) ).toBeNull(); + const restoredStalker = await readPlaylist( + app.mainWindow, + STALKER_PLAYLIST_ID + ); + expect(restoredStalker).toMatchObject({ + _id: STALKER_PLAYLIST_ID, + password: STALKER_PASSWORD, + stalkerIdentityOverrides: { + deviceId1: 'STRUCTURED-BACKUP-DEVICE-1', + serialNumber: STALKER_STRUCTURED_SERIAL, + }, + username: STALKER_USERNAME, + }); + expect(restoredStalker).not.toHaveProperty('stalkerAccountInfo'); + expect(restoredStalker).not.toHaveProperty('stalkerLandingUrl'); + expect(restoredStalker).not.toHaveProperty( + 'stalkerLastVerifiedAt' + ); + expect(restoredStalker).not.toHaveProperty( + 'stalkerRecipeClassifierVersion' + ); + expect(restoredStalker).not.toHaveProperty( + 'stalkerRequestRecipe' + ); + expect(restoredStalker).not.toHaveProperty('stalkerToken'); + const allPlaylists = await readAllPlaylists(app.mainWindow); expect(allPlaylists).toHaveLength(2); expect( diff --git a/apps/electron-backend-e2e/src/support/backup-security.ts b/apps/electron-backend-e2e/src/support/backup-security.ts index c2ed44a54..b951475f1 100644 --- a/apps/electron-backend-e2e/src/support/backup-security.ts +++ b/apps/electron-backend-e2e/src/support/backup-security.ts @@ -72,6 +72,10 @@ export async function seedProviderPlaylists(page: Page): Promise { id: 'mag250-public-5_1-minimal-v1', version: 1, }, + stalkerLandingUrl: 'https://stalker-backup.test/c/', + stalkerLastVerifiedAt: '2026-07-27T10:00:00.000Z', + stalkerRecipeClassifierVersion: 1, + stalkerRequestRecipe: 'stateless-mac', stalkerSerialNumber: STALKER_LEGACY_SERIAL, stalkerSignature1: STALKER_LEGACY_SIGNATURE_1, stalkerSignature2: STALKER_LEGACY_SIGNATURE_2, diff --git a/libs/services/src/lib/playlist-backup.service.stalker-learned-state.spec.ts b/libs/services/src/lib/playlist-backup.service.stalker-learned-state.spec.ts new file mode 100644 index 000000000..df5b1ecd9 --- /dev/null +++ b/libs/services/src/lib/playlist-backup.service.stalker-learned-state.spec.ts @@ -0,0 +1,95 @@ +import { of } from 'rxjs'; +import { + Playlist, + PlaylistBackupManifestV1, + PLAYLIST_BACKUP_KIND, + PLAYLIST_BACKUP_VERSION, + StalkerPlaylistBackupEntry, +} from '@iptvnator/shared/interfaces'; +import { createPlaylistBackupService } from './playlist-backup.service.test-helpers'; + +describe('PlaylistBackupService Stalker learned-state restore', () => { + it.each([false, true])( + 'clears excluded learned connection state when merging a %s-secret backup', + async (includeSecrets) => { + const local: Playlist = { + _id: 'stalker-backup', + title: 'Local Stalker', + count: 1, + importDate: '2026-07-01T00:00:00.000Z', + lastUsage: '2026-07-01T00:00:00.000Z', + autoRefresh: false, + portalUrl: 'https://portal.test/server/load.php', + stalkerSourceUrl: 'https://portal.test/c/', + macAddress: '00:1A:79:AA:BB:CC', + stalkerProfilePreset: { + id: 'mag250-public-5_1-minimal-v1', + version: 1, + }, + username: 'local-user', + password: 'local-password', + stalkerIdentityOverrides: { + serialNumber: 'LOCAL-SERIAL', + }, + stalkerLandingUrl: 'https://portal.test/c/', + stalkerLastVerifiedAt: '2026-07-27T10:00:00.000Z', + stalkerRecipeClassifierVersion: 1, + stalkerRequestRecipe: 'stateless-mac', + } as Playlist; + const backupEntry: StalkerPlaylistBackupEntry = { + portalType: 'stalker', + exportedId: local._id, + title: 'Restored Stalker', + autoRefresh: false, + connection: { + portalUrl: local.portalUrl as string, + sourceUrl: local.stalkerSourceUrl, + macAddress: local.macAddress as string, + profilePreset: local.stalkerProfilePreset, + ...(includeSecrets + ? { + identityOverrides: + local.stalkerIdentityOverrides, + username: local.username, + password: 'replacement-password', + } + : {}), + }, + userState: { + favorites: [], + recentlyViewed: [], + }, + }; + const manifest: PlaylistBackupManifestV1 = { + kind: PLAYLIST_BACKUP_KIND, + version: PLAYLIST_BACKUP_VERSION, + exportedAt: '2026-07-27T00:00:00.000Z', + includeSecrets, + playlists: [backupEntry], + }; + const playlistsService = { + addPlaylist: jest.fn((playlist: Playlist) => of(playlist)), + getAllData: jest.fn(() => of([local])), + getRawPlaylistById: jest.fn(() => of('#EXTM3U')), + handlePlaylistParsing: jest.fn(), + }; + const service = createPlaylistBackupService({ + playlistsService, + }); + + await expect( + service.importBackup(JSON.stringify(manifest)) + ).resolves.toMatchObject({ imported: 0, merged: 1, failed: 0 }); + + expect(playlistsService.addPlaylist).toHaveBeenCalledWith( + expect.objectContaining({ + _id: local._id, + stalkerLandingUrl: undefined, + stalkerLastVerifiedAt: undefined, + stalkerRecipeClassifierVersion: undefined, + stalkerRequestRecipe: undefined, + }) + ); + } + ); +}); diff --git a/libs/services/src/lib/playlist-backup.service.ts b/libs/services/src/lib/playlist-backup.service.ts index b5a80eaa0..7f6863cc9 100644 --- a/libs/services/src/lib/playlist-backup.service.ts +++ b/libs/services/src/lib/playlist-backup.service.ts @@ -1215,6 +1215,10 @@ export class PlaylistBackupService { ...(hasOwn(entry.connection, 'stalkerSignature2') ? { stalkerSignature2: entry.connection.stalkerSignature2 } : {}), + stalkerLandingUrl: undefined, + stalkerLastVerifiedAt: undefined, + stalkerRecipeClassifierVersion: undefined, + stalkerRequestRecipe: undefined, stalkerToken: undefined, stalkerAccountInfo: undefined, };