74 KiB
Stalker Session Compatibility Foundation Design
Status
Approved for specification on 2026-07-27.
This document defines Stage 1 of the Stalker/Ministra compatibility work. It turns the authentication findings from the Stalker client audit into an Electron-first runtime design. It does not authorize implementation until this written specification has passed user review and a separate implementation plan has been approved.
Context
IPTVnator currently has two Stalker request paths:
- a stateless/simple
portal.phppath; - a full-portal path whose handshake, token cache, profile calls, refresh, and watchdog are orchestrated in the Angular renderer while Electron main performs the HTTP request.
The full-portal path has correctness and ownership problems:
- it decides whether a portal is "full" from URL substrings rather than resolving the actual API endpoint;
- its first profile request incorrectly sends
auth_second_step=1; - it does not branch on profile status, so status
2never performs the requireddo_authfollowed by a second profile; - the existing
do_authhelper sends blank credentials and is unused; - token state is renderer-owned while redirects and HTTP transport are main-owned;
- server-issued cookies are not retained in a real portal-scoped cookie jar;
- identity, transport, locale, and firmware fields do not form one coherent device profile;
- unsupported synthetic identity behavior, including a derived
__cfduid, can make a portal reject or bind an account incorrectly; - saved tokens are stale configuration rather than a trustworthy session;
- generic authentication errors hide whether failure occurred during endpoint discovery, device verification, credentials, portal protection, or account authorization;
- existing mocks exercise catalogs but do not accurately replay
get_profile, status2, cookie rotation, redirect, or refresh behavior.
The desktop application is the first target because Electron main already owns validated redirects, DNS pinning, native playback launch, and the process boundary suitable for cookies and bearer tokens. The self-hosted PWA keeps its current path during Stage 1. Pure protocol rules and fixtures remain reusable for a later PWA backend adapter.
Approved Product Decisions
- Stage 1 is Electron-first. Existing PWA and simple-portal behavior must not regress, but full server-side PWA session parity is not an acceptance requirement.
- The live full-portal session is owned by Electron main.
- Pure resolver rules, auth transitions, response classification, session-key inputs, and profile presets live in a provider-scoped utility library.
- Login and password fields are disclosed only when the portal returns profile
status
2or when the user explicitly expands the portal-login section. - New credentials are persisted only after
do_authand a successful second profile. - Stage 1 uses the existing playlist credential storage model. A secure
Keychain/
safeStoragemigration belongs to a separate, cross-provider project rather than a Stalker-only vault. - Structured Xtream/Stalker credentials and optional/account-binding Stalker device identity beyond the required MAC are exported only after an explicit backup choice. The export choice is off by default.
- Tokens, server cookies, handshake random values, leases, and challenges are never persisted.
- The application does not invent serial numbers, device IDs, signatures, Cloudflare cookies, or opaque firmware hashes.
- Identity profiles are stable and versioned. Failure never triggers automatic MAC, serial, firmware, or profile cycling.
Goals
- Resolve root, landing, custom-prefix,
portal.php, andserver/load.phpinputs safely. - Select a tested
full-sessionorstateless-macrequest recipe without URL substring heuristics. - Preserve the winning handshake and its server-issued cookie state.
- Implement the canonical profile status
0/1/2state machine. - Keep bearer tokens, handshake random values, and cookie jars in Electron main.
- Scope a live session to the actual portal and stable device identity rather than a playlist row.
- Serialize refresh so concurrent authorization failures produce one refresh generation and at most one retry per operation.
- Replace generic authentication failures with stable, stage-aware outcomes.
- Preserve simple Stalker and PWA request paths.
- Build sanitized replay fixtures before relying on new runtime behavior.
- Lazily upgrade existing full-portal playlists without a bulk rewrite.
- Establish an opaque playback-context seam so renderer code no longer builds
secret Stalker
CookieorAuthorizationheaders.
Non-goals
- PWA backend session ownership or Electron/PWA full-auth parity.
- A secure credential vault or migration of Xtream credentials at rest.
- Catalog SQLite caching, offline-first catalogs, FTS, or last-good sync snapshots.
- Pagination redesign, page-size learning, repeated-page recovery, or partial catalog reconciliation.
- HTTP
462recovery, previous-stream release, or a general playback-session lifecycle. - Automatic device-emulation recipes or guessing opaque native MAG values.
- Capturing, storing, or publishing traffic from a portal the user does not own or control.
The cache and pagination work is Stage 2. Evidence-driven create_link, redirect,
HTTP 462, and stream-release behavior is Stage 3.
Considered Architectures
Renderer-owned state machine
Angular would continue to own handshake, token, profile, and refresh while Electron gained a cookie jar. This minimizes migration but leaves session state split between processes. Atomic redirect validation, cookie mutation, single-flight refresh, watchdog scheduling, and playback context cannot be reasoned about as one unit. Tokens would remain visible to renderer code.
This option is rejected.
Electron session manager with a pure protocol core
A provider-scoped pure library defines protocol rules. Electron main owns live session state and HTTP. Angular becomes a facade over opaque leases and challenges. The PWA retains its legacy adapter.
This is the selected design. It provides the correct process boundary without making a new PWA backend a prerequisite.
Shared host runtime for Electron and PWA
The complete stateful runtime could be shared between Electron and a web backend through transport adapters. This offers strict parity but requires server-side browser-session isolation, opaque web handles, process-affinity semantics, and a credential contract outside this repository.
This remains a possible PWA follow-up, not Stage 1.
Component Boundaries
libs/portal/stalker/protocol
Create an Nx project with tags scope:portal, domain:stalker, and
type:util, exposed through a scoped path alias. It has no Angular, Electron,
Axios, Node HTTP, persistence, timer, or logging dependency.
It owns:
- URL normalization and versioned endpoint-candidate recipes;
- normalized profile status and body-error classification;
- pure auth state transitions;
- stable failure codes and operation stages;
- coherent, versioned identity/profile presets;
- canonical session identity input and identity-revision serialization;
- request parameter rules, including reserved auth parameters;
- fixture-facing protocol types that contain no runtime secrets.
It does not perform network requests, store state, or make UI decisions.
apps/electron-backend/src/app/services/stalker-session/
Electron main owns the stateful runtime:
stalker-session-managerowns the session registry, leases, generations, and single-flight work;stalker-endpoint-resolverperforms bounded, validated discovery using the pure candidate rules;stalker-http-sessionintegrates the cookie jar with every validated redirect hop;stalker-auth-sessionexecutes handshake, profile, optionaldo_auth, and refresh;stalker-cookie-jarowns managed bootstrap cookies and serverSet-Cookiestate;stalker-challenge-registryowns one-time origin and credential challenges;stalker-watchdogschedules profile-derived refresh checks only while a session is active;stalker-playback-contextassociates a successfulcreate_linkresult with main-owned request headers without exposing them to Angular.
The cookie implementation must use a direct runtime dependency such as
tough-cookie. It must be integrated manually with the existing validated
redirect helper. axios-cookiejar-support must not replace or bypass the
current DNS-pinned agents and per-hop URL validation.
IPC and preload
Add typed full-session operations:
STALKER_SESSION_OPEN;STALKER_SESSION_CONTINUE;STALKER_SESSION_REQUEST;STALKER_SESSION_CONTROL.
Shared DTOs belong in @iptvnator/shared/interfaces. The IPC handlers validate
the payload at runtime, bind references to the sending webContents, and call
the session manager. The preload surface exposes only the typed operations.
The existing STALKER_REQUEST path remains for stateless/simple portals and the
legacy PWA adapter. Migrated Electron full-portal call sites cannot send raw
handshake, get_profile, or do_auth actions through the new request
operation.
Angular Stalker data access
StalkerSessionService becomes a thin facade:
- convert playlist data into a typed connection descriptor;
- map a
full-sessionplaylist to an opaqueleaseRef; - expose origin-approval, credential, progress, ready, and failure outcomes;
- activate, deactivate, close, or force-redetect a lease;
- route full-portal catalog/search/detail/create-link operations through
STALKER_SESSION_REQUEST; - keep the current simple/PWA adapter available.
It does not own a token cache, handshake random, cookies, profile timers, raw auth requests, or secret playback headers.
Provider stores and features continue to depend on Stalker data access rather than Electron APIs directly.
Import and settings UI
The Stalker import feature owns:
- connection progress;
- exact source/final origin confirmation;
- portal-login disclosure;
- masked password input with a reveal control;
- the advanced identity section;
- persistence after a confirmed ready outcome;
- lazy upgrade while importing or explicitly testing a connection.
The backup settings surface owns the explicit structured credential/device identity export choice and its residual-sensitivity warning.
Challenges raised while opening an existing Stalker route are owned by a
route-level connection-flow service in portal-stalker-feature, not by the
import component. It presents the same origin/credential outcomes through
dialogs, coordinates lazy persistence with data access, and discards the
provisional attempt on cancel or route navigation. Stalker data access remains
UI-free.
tools/stalker-fixtures
Create a Node-only Nx tool project named stalker-fixture-tools, tagged
scope:tools, domain:stalker, and type:tool. It owns the deterministic
fixture validator, fail-closed secret scanner, and local HAR-to-draft converter.
It is never imported by production renderer or Electron bundles.
Session Identity, Leases, and Lifecycle
The internal live-session key is derived from:
canonical API endpoint
+ normalized uppercase colon-delimited MAC
+ stable identity revision
+ confirmed principal discriminator
The identity revision is a canonical digest input containing:
- profile preset ID and version;
- explicit serial, device IDs, and signatures;
- effective User-Agent and X-User-Agent overrides;
- effective Referer and Origin policy/overrides;
- effective locale, language, timezone, and other identity-affecting profile overrides.
Playlist ID, source URL aliases, token, handshake random, cookies, account
information, and password are not part of the key. The principal discriminator
is mac-only for a status-0 session or a stable digest of the normalized
username confirmed by a successful status-2 flow. The raw username and password
are never map keys.
For this purpose, "normalized username" means the length-prefixed UTF-8 value that was actually submitted and accepted. It is not trimmed, case-folded, or Unicode-normalized, because a portal may treat those transformations as different principals.
Discovery and authentication attempts are provisional and never join an
existing ready session. After network ready, the manager computes the
destination key from the confirmed principal but retains the session in the
attempt namespace. Only commit inserts or replaces it in the ready-session
registry. Two playlist rows may share a committed session only when endpoint,
MAC, identity revision, and principal all match. Rows with different usernames
never share a jar, token generation, or account summary.
The renderer receives a cryptographically opaque leaseRef, never the internal
session key. Each lease is bound to its creating webContents and playlist
facade instance. Two playlist rows resolving to the same session key may have
separate leases over the same session. Closing one lease does not break another.
Opening or testing edited endpoint, identity, or credential values creates a separate provisional attempt and jar. It does not mutate the current local session object. Some servers nevertheless rotate one token globally per MAC, so client-side jar isolation is not sufficient. The manager therefore owns a base-identity coordinator keyed only by approved portal origin and normalized MAC. This intentionally serializes different profile revisions on the same origin/MAC because the server may key its token only by MAC.
The coordinator tracks a monotonic mutation epoch, one wire-active principal, and a read/write gate:
- requests for the current principal and epoch may run concurrently as readers;
- handshake, refresh, principal switching, and provisional authentication are exclusive mutations;
- a session whose recorded epoch or principal is no longer active is suspended and cannot send or refresh with stale auth state;
- an operation for a suspended principal first authenticates that principal under the exclusive gate, advances the epoch, then runs against the resulting generation before another principal can switch;
- only the wire-active principal owns a watchdog; inactive logical sessions retain their jar and accepted credentials in memory but do not create competing refresh traffic.
A provisional session may become stale while it waits up to two minutes for
local persistence, because an existing lease is allowed to switch the active
principal rather than freezing the application. commit therefore acquires the
exclusive gate and revalidates the provisional principal whenever its ready
epoch is no longer current before promoting it. discard restores or lazily
revalidates the previous active session. A failed revalidation after the draft
was persisted follows the existing session-promotion-failed recovery path.
If the destination session key already exists, commit atomically replaces its auth generation, transfers all existing leases to the promoted session, and invalidates old playback contexts; it never leaves two same-principal session objects. Different principals remain separate logical sessions but switch through the same base coordinator. This deliberately favors correctness over parallelism for the unusual same-portal/same-MAC/multiple-account case.
Session lifecycle rules:
activatestarts or retains the profile-derived watchdog;deactivatestops activity attributable only to that lease;commitpromotes a persisted provisional ready session;discarddestroys a provisional attempt and restores/revalidates the prior ready session when necessary;closereleases the lease;- successfully committed identity, endpoint, principal, or credential changes invalidate the replaced auth generation;
- playlist deletion and renderer destruction release their leases;
- application shutdown destroys all jars and session state;
- Stage 1 keeps inactive sessions only for the current application process and does not persist or implement an additional idle cache.
Endpoint Discovery
Discovery has two phases.
Landing phase
The renderer supplies an HTTP(S) source URL and non-secret presentation
metadata. Electron normalizes the URL, rejects embedded basic-auth credentials
and credential/auth query keys such as username, password, token, mac,
serial, device_id, or signature, rejects unsupported schemes, and strips
the fragment. A non-sensitive landing query may be used for that landing
request but is never copied into derived API candidates. Electron performs the
validated landing request without:
- MAC;
- bearer token;
- login or password;
- serial, device ID, or signature;
- identity-specific cookies or headers.
Redirects are followed manually through the existing SSRF/DNS-pinning policy. The existing five-hop redirect ceiling remains in force.
The landing phase owns an anonymous discovery jar. It may retain server cookies set by the secret-free landing flow, but it never contains managed MAC cookies or credentials.
If the final origin differs from the source origin, discovery returns an
origin-approval-required outcome before any identity-bearing probe. The
challenge is bound to the sender, exact source origin, exact final origin, and
the discovered landing path. It is single-use and expires after two minutes.
Candidate phase
After the origin is trusted, a versioned recipe table derives at most six API candidates:
- an input URL that is already
portal.phporserver/load.php; - for a resolved
/c,/c/, or/c/index.*landing, the parentserver/load.php, then the parentportal.php; - for another resolved document/directory, same-directory
server/load.php, then same-directoryportal.php; - for a root landing only, the conventional
stalker_portal/server/load.php, thenstalker_portal/portal.php.
Duplicates are removed while preserving order. Candidates are evaluated sequentially under the stateless downgrade guard below. Each candidate starts with an isolated temporary cookie jar so a failed probe cannot poison another candidate. The temporary jar receives only anonymous discovery cookies applicable to that candidate URL; candidate mutations remain isolated.
A candidate probe is a MAC-only handshake after the origin has been approved. It does not contain bearer authorization, password, or optional device identity. Success requires:
- an allowed HTTP status;
- a content-type/body pair accepted by the response media policy;
- a valid handshake body containing a usable token;
- no classified portal-protection or body-level failure.
After a valid handshake, the same temporary session executes the first profile.
A recognized profile status or recognized status-less success proves a
full-session endpoint. Its handshake token, optional random, first-profile
result, endpoint, landing Referer, and temporary jar are promoted atomically.
The manager must not discard either response and repeat the handshake or first
profile.
A benign endpoint-shape failure may advance to the next candidate. A recognized authorization, credentials, account/device, portal-protection, rate-limit, or transport failure stops resolution and can never become evidence for a stateless downgrade.
Stateless recipe classification
A portal that explicitly does not support handshake/profile actions may still be a valid stateless MAC portal. For each candidate, the resolver records a stateless fallback only when:
- the handshake or first-profile action is explicitly unsupported rather than rejected for authorization, credentials, account, identity, WAF, rate limit, or transport reasons; and
- the existing read-only
type=itv&action=get_genrescapability probe returns a recognized{ js: [...] }simple-portal shape.
The probe occurs only after origin approval and may contain the managed MAC identity, but never bearer authorization, credentials, or optional device identity. Its temporary jar is discarded after classification.
The resolver continues through every later candidate looking for
full-session, even after recording a valid stateless fallback. It selects the
first recorded stateless-mac candidate only after every candidate has been
exhausted without full-session evidence and no candidate produced a failure
that forbids downgrade. Thus an early permissive get_genres response cannot
mask a later working server/load.php or portal.php full endpoint.
The selected recipe becomes part of the successful connection outcome.
full-session uses the new session manager; stateless-mac uses the preserved
simple request adapter. There is no stateless fallback after a full
authentication rejection. Existing simple playlist records remain on their
current adapter without being forced through a full-session migration.
If a previously learned endpoint returns 404, HTML, or an incompatible body,
one secret-free rediscovery is allowed. No operation may perform more than one
rediscovery.
Response media policy
All response classifiers normalize the media type to lowercase and ignore only valid MIME parameters. Body-size enforcement happens before parsing.
application/jsonandtext/jsonaccept JSON only;application/javascriptandtext/javascriptaccept JSON or the exact allowlisted JSONP envelopes;text/plainand a missingContent-Typemay use bounded compatibility sniffing, but only an exact recognized JSON/JSONP envelope can become protocol success;- every other type, including
text/htmlandapplication/octet-stream, is ineligible for protocol success.
Actual HTML/WAF bodies are classified as portal protection when their evidence
is recognized. A syntactically valid JSON body mislabeled text/html is not
silently accepted: it is incompatible-response and may trigger the one
learned-endpoint rediscovery. The fixture matrix asserts these exact
distinctions.
Cookie Ownership
Each live session has one in-memory RFC-aware cookie jar.
Managed bootstrap cookies are derived from the effective profile:
mac;stb_lang;timezone.
Managed cookies override same-named server cookies. A server response cannot
shadow a managed name through a narrower Domain or Path: every Set-Cookie
mutation for mac, stb_lang, or timezone is discarded, regardless of its
attributes, and the manager materializes exactly one effective value for each
managed name at request time. Other server-issued Set-Cookie values are
retained with their Domain, Path, Secure, HttpOnly, expiry, and redirect-hop URL
semantics.
Rules:
- every response hop may mutate only the temporary or active jar that issued the request;
- before a cross-origin redirect, remove bearer authorization, Cookie, managed MAC/device parameters, X-User-Agent, SN, Origin, Referer, credentials, and every optional identity field;
- normalize the cross-origin
Locationwith the same user-info, fragment, and credential/auth-query rejection used for source URLs; source-origin query secrets are never copied into target discovery; - a cross-origin redirect discovered during an identity-bearing operation pauses that operation behind a new origin challenge; only secret-free discovery may continue far enough to identify the target;
- approving that target restarts endpoint resolution and authentication in a
fresh candidate jar for the approved origin; it never replays the redirected
handshake, profile,
do_auth, or catalog request with source-origin credentials, bearer state, cookies, or optional identity attached; - previously entered credentials may be submitted to the approved origin only
after its own fresh profile returns status
2; - an unapproved origin never receives managed identity cookies;
- endpoint or identity invalidation destroys the jar;
- cookie state is never written to playlist data, backups, diagnostics, or local storage;
- the synthetic serial-derived
__cfduidis removed; SNis not a default HTTP header and is available only as an explicit, versioned compatibility override.
Coherent Identity Profiles
Stage 1 ships one automatic public-shape preset,
mag250-public-5_1-minimal-v1. It explicitly targets the public Stalker 5.1
request shape; it is not presented as an exact physical MAG250 fingerprint.
"Coherent" means every declared value describes that one shape, not that
unknown native fields are filled with guesses. The preset fixes:
- the existing
Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250browser User-Agent as a named legacy compatibility value; public portal JavaScript does not establish this exact native browser UA, so the preset does not call it canonical; X-User-Agent: Model: MAG250, rather than copying the browser User-Agent into this model header;stb_type=MAG250,client_type=STB, andhd=1;- metrics containing MAC,
model=MAG250, andtype=STB, plus handshake random, serial, and UID only when present or explicitly configured; UID is sourced only from the configureddevice_id2; - the resolved landing Referer;
- one normalized locale/language/timezone tuple used consistently by
Accept-Language,stb_lang, the timezone cookie, and the parameters declared by this versioned profile; it does not invent a genericlocalefield; - session-derived
auth_second_step,not_valid_token, and timestamp values.
Fields whose real values require native MAG APIs remain absent by default:
ver, image_version, hw_version, hw_version_2, num_banks, video_out,
device IDs, signatures, prehash, and api_signature. Omitting those keys,
rather than sending empty or zero pseudo-values, is an intentional safety
deviation from a physical 5.1 device request.
Public Stalker 4.9 and 5.1 profile shapes are not interchangeable. A strict 4.9 portal may require a complete firmware tuple that cannot be reconstructed from the MAC. Such a portal uses an explicit traced/custom profile; the runtime does not automatically retry a 5.1 failure with a synthetic 4.9 recipe. A future automatic preset may add native-derived values only when one internally coherent set is supported by a controlled fixture and documented evidence.
Primary references:
- Stalker 5.1 device values and X-User-Agent construction;
- Stalker 5.1 handshake, metrics, and profile request;
- Stalker 4.9 profile request shape;
- Stalker 4.9 firmware/metrics validation.
Advanced identity values are explicit user configuration. Blank serial, device-ID, signature, and prehash fields are omitted rather than generated. The runtime forwards explicit values unchanged in the protocol locations defined by the selected profile.
The runtime does not:
- derive device IDs from a MAC;
- use
SHA1(MAC)as a universal prehash; - invent opaque firmware/device signatures;
- rotate profile presets after rejection;
- infer a serial from a MAC;
- synthesize Cloudflare cookies.
Existing playlists with explicit values are mapped to a stable custom profile revision. Existing blank values remain absent.
Authentication State Machine
flowchart TD
A["NEW"] --> B["RESOLVING"]
B --> C{"Origin changed?"}
C -->|"yes"| D["AWAITING_ORIGIN_APPROVAL"]
C -->|"no"| E["HANDSHAKING"]
D --> E
E --> F["PROFILE_FIRST: auth_second_step=0"]
F -->|"status 0 / recognized normal"| G["READY"]
F -->|"status 1 / blocked"| H["REJECTED"]
F -->|"status 2"| I["AWAITING_CREDENTIALS"]
I --> J["DO_AUTH"]
J -->|"js === true"| K["PROFILE_SECOND: auth_second_step=1"]
J -->|"explicit credential rejection; attempts remain"| I
J -->|"explicit rejection; attempt limit"| H
J -->|"transport / WAF / rate limit"| N["TYPED FAILURE"]
K -->|"recognized normal"| G
K -->|"status 2 / credential rejection; attempts remain"| I
K -->|"status 1 / account block / attempt limit"| H
K -->|"transport / incompatible response"| N
G --> L["Authenticated requests + watchdog"]
L -->|"proven token rejection"| M["REFRESHING"]
M --> E
Profile statuses accept numeric and string forms. Status 0 is ready, status
1 is rejected or blocked, and status 2 requires credentials. A missing
status is accepted only for a recognized profile-shaped success response with
no credential, block, or body-error indicators. Unknown status values are
incompatible-response.
The first profile always sends auth_second_step=0. do_auth is legal only
after status 2. A second profile is legal only after canonical do_auth
success and always sends auth_second_step=1. A successful do_auth response
alone never marks the session ready. Canonical success requires the normalized
response to contain js === true; other shapes require an explicit fixture and
classifier rule rather than JavaScript truthiness.
do_auth contains login/password plus only the explicit device identity fields
defined by the active profile. Blank device IDs or signatures remain absent,
and the second profile reuses the identical identity revision.
For an existing playlist with saved credentials, Angular may answer the
credential challenge automatically after the trusted endpoint is known. A new
or edited password is sent only through STALKER_SESSION_CONTINUE. It is
persisted only after the second profile reaches ready. A failed replacement
does not overwrite previously stored credentials.
Each credential submission consumes its one-time challenge. An explicit
credential rejection returns a fresh challenge over the same trusted endpoint
and candidate jar, up to three submissions per connection attempt. Rejection of
saved credentials expands the login form. A fourth submission requires a new
connection attempt and returns credentials-attempt-limit.
Timeout, DNS/TLS/network failure, WAF response, 429, or 5xx during
do_auth is not a credential rejection. It keeps stored credentials unchanged
and returns its transport/protection outcome. 429 terminates the attempt and
honors Retry-After; other retryable failures may restart through the normal
open flow without reusing a consumed challenge.
After ready, Electron may retain the accepted credentials in that in-memory session only so a later status-2 refresh can complete without another UI round-trip. The copy is cleared with the session, is never part of its key, and is never logged or persisted by Electron.
The handshake token has no invented TTL. IPTVnator does not persist it even if
the profile advertises store_auth_data_on_stb; protected token persistence is
outside Stage 1.
Refresh and Watchdog
A request can trigger refresh only from proven token-rejection evidence:
- HTTP
401; - an exact supported HTTP-200
Authorization failedbody/envelope; - a
403response classified as token-auth rejection rather than WAF or account denial.
Access denied, status 1, WAF-style HTML, 429, network errors, and 5xx
responses do not rotate the token.
Refresh is generation-aware and single-flight:
- the first failing operation invalidates the current auth generation;
- concurrent failures join the same refresh promise;
- refresh reruns the canonical handshake/profile flow;
- the refreshed flow must confirm the same principal discriminator;
- the original operation retries once against the new generation;
- another rejection is terminal for that operation.
There is no recursive refresh path.
The principal discriminator of a committed session is immutable. If a
protocol-valid refresh resolves a different discriminator, or a mac-only
session reaches status 2 before the committed principal can be reconfirmed,
the manager does not re-key in place and does not retry the original operation.
It discards that refresh result and returns
principal-transition-required. The base coordinator advances its epoch,
enters a no-active-committed-principal transition state, and suspends old leases
so none can send the now-stale generation. The route-level connection flow
opens a provisional replacement, handles status 2 if necessary, persists it,
and commits through the normal collision/coordinator path. A transition to
mac-only does not silently erase stored credentials. This keeps lease,
account-summary, jar, and session-key ownership atomic.
A status-2 refresh for an existing username principal may use its retained
credentials or a challenge constrained to that exact username. Submitting a
different username is a provisional principal change, not an in-place refresh.
Transport, protection, rate-limit, and incompatible-response failures retain
their own typed outcomes and never become principal-transition-required.
The watchdog runs in Electron main, not Angular. Its interval is derived from recognized profile fields and clamped to safe minimum and maximum bounds with jitter. Invalid or missing values use one documented conservative default. Watchdog authorization failure joins the same refresh mechanism. Other watchdog failures are classified and surfaced without silently changing identity.
IPC Contract
Open
STALKER_SESSION_OPEN accepts a connection descriptor containing:
- playlist reference used only for facade correlation;
- connection mode: persisted-open or provisional import/edit/migration;
- source URL and optional learned endpoint hint;
- normalized MAC;
- selected profile preset and explicit overrides;
- non-secret transport configuration.
It returns one discriminated outcome:
readywith the resolved endpoint and selected recipe; provisional outcomes include an opaqueattemptRef;full-sessionalso includesleaseRef, safe account summary, and capabilities, whilestateless-maccarries a persistence draft but no session lease;origin-approval-requiredwith an opaque challenge and display-safe origins;credentials-requiredwith an opaque challenge;failurewith a stable reason, stage, retryability, and request ID.
Continue
STALKER_SESSION_CONTINUE accepts an opaque challenge and exactly one response:
- approval or rejection of the displayed final origin; or
- login/password for a credential challenge.
Challenges are sender-bound, single-use, expire after two minutes, and are
invalidated when their connection attempt terminates.
Continuation returns the same ready, next-challenge, or failure
discriminated outcome family as open.
Request
STALKER_SESSION_REQUEST accepts:
- a sender-bound
leaseRef; - a typed application operation;
- operation-specific, non-reserved parameters.
The renderer cannot supply or override:
- token, prehash,
auth_second_step, login, or password; - handshake/profile/do-auth actions;
- managed MAC, cookie, identity, locale, or firmware fields;
- raw Authorization or Cookie headers.
The manager constructs the wire request from the active session.
The return value is either a sanitized typed operation result, the same
origin/credential challenge shape used by open, or a typed failure. A challenge
terminates the current application operation; main does not retain or
implicitly replay its catalog/search/playback parameters. Continuation may
produce a provisional ready outcome for the replacement session. After the
route-level flow persists and commits that outcome, the Angular facade may
reissue the original typed operation once under the same explicit retry budget.
A refresh that newly reaches status 2 from a mac-only session does not use
this in-place continuation path: it returns
principal-transition-required, starts the route-level provisional reconnect,
and reissues the operation only after persistence and commit.
Control
STALKER_SESSION_CONTROL supports:
activate;deactivate;commit;discard;close;force-redetect.
References are never accepted from a different renderer and are not logged.
commit is accepted only after the renderer reports that the corresponding
playlist draft was persisted successfully. A ready provisional attempt may
remain for two minutes while the UI retries a failed local save. discard is
mandatory on cancel, navigation, expiry, or abandonment of that retry.
Playback Context Seam
Full-portal create_link runs through the session manager. On success, Electron
main records the effective session-owned headers against the exact returned
stream and its bounded playback context. The renderer receives the stream URL,
safe presentation metadata, and, when needed, an opaque playback context
reference. It does not receive bearer or cookie headers.
The context is bound to the creating renderer, lease, session key, auth generation, and exact normalized stream URL. An unclaimed reference expires after two minutes. A successful authorized main-process launch consumes the reference; refresh, lease close, renderer destruction, endpoint change, or identity change invalidates it first. Advancing the base-identity mutation epoch, including a switch to another principal on the same MAC, invalidates all unclaimed contexts under that coordinator. The native player may retain the resolved headers for that launched playback, but they never return through preload.
The external MPV/VLC and Embedded MPV launch paths resolve the context in main. Renderer playback metadata may still contain non-secret User-Agent, Referer, and Origin values needed by existing player abstractions.
Stage 1 removes the current renderer call to getCachedToken() and stops
constructing synthetic Stalker playback cookies in data access. It preserves
current successful playback behavior but does not add HTTP 462, previous
stream release, or general playback-session recovery. Those require Stage 3
fixtures.
Persistence Model
User-owned configuration
Playlist storage keeps:
stalkerSourceUrl, the URL entered by the user;- MAC;
- selected profile preset ID;
- explicit serial/device/signature/profile and transport overrides;
- username/password after confirmed authentication;
- existing portable user state.
Learned non-secret hints
After a complete ready outcome, local playlist storage may update:
portalUrl, the last verified canonical API endpoint retained for current consumer compatibility;stalkerLandingUrl, the resolved landing URL used for Referer behavior;stalkerRequestRecipe, exactlyfull-sessionorstateless-mac;stalkerRecipeClassifierVersion, the version that established that recipe;- the last verified timestamp;
- safe account summary already supported by the playlist model.
Learned hints are performance hints, never proof of trust. They are revalidated before identity-bearing traffic and may trigger one secret-free rediscovery.
Ephemeral state
The following remain in Electron memory only:
- bearer token and handshake random;
- server cookies;
- accepted credentials needed for an authenticated status-2 refresh;
- leases and challenges;
- refresh generations and pending promises;
- watchdog state;
- active playback contexts.
Backup Contract
Stage 1 keeps backup manifest version 1 and adds optional fields backward-compatibly. Existing version-1 manifests remain importable. The Stalker connection adds:
- source URL;
- profile preset ID and version;
- portable non-secret profile/transport overrides, including User-Agent, Referer, Origin, locale, language, and timezone settings;
- the existing compatibility
portalUrl, MAC, andisFullStalkerPortalfields.
Restore prefers source URL and resolves the endpoint again. Learned landing, request recipe/classifier version, account summary, and runtime session state are not restored as authoritative state.
The export UI adds an explicit
Include portal credentials and device identity choice, off by default. When
enabled, it includes:
- structured Xtream/Stalker usernames and passwords;
- explicit Stalker serial, device IDs, signatures, prehash/native hash values, and custom firmware identity tuple.
The manifest includeSecrets value reflects this choice. A manifest declaring
includeSecrets=false but containing one of those gated fields is rejected
rather than silently trusting inconsistent metadata.
The version-1 Xtream connection currently requires a username, so redaction needs an explicit additive shape rather than an invalid half-entry. A new version-1 writer uses these conditional rules:
- with
includeSecrets=true, Xtream keeps its existing non-empty username and optional password shape; - with
includeSecrets=false, Xtream omits both fields and writescredentialsOmitted: truebeside the non-secret server URL; - a missing Xtream username is valid only in that exact redacted form; old version-1 entries remain valid and unchanged.
On restore, a redacted Xtream entry never creates an unusable playlist. If its
exportedId, portal type, and normalized server URL identify the same existing
local playlist, restore preserves that playlist's local credentials and applies
the backed-up user state. Otherwise the backup UI creates an in-memory pending
restore item, collects username/password, validates them through the normal
Xtream connection flow, and only then creates or matches the playlist and
applies its pending user state. Skipping the prompt reports that entry as
skipped; it does not persist a credential-less Xtream row. Server URL alone is
never enough to merge two redacted Xtream accounts.
MAC, portal/source URL, non-secret transport overrides, and raw M3U content are not controlled by this checkbox. They can still be sensitive. Raw M3U content can itself contain signed or private URLs and is the canonical restore artifact. Stage 1 does not rewrite raw M3U text. The export UI therefore warns precisely that the file still contains playlist MACs, hosts/source URLs, and potentially private raw M3U URLs even when structured account-binding secrets are excluded.
Restoring a Stalker entry without credentials is valid. Unlike Xtream, its MAC
is already the primary connection identity; if the resolved portal returns
status 2, the normal credential challenge collects the omitted login and
password.
Merge semantics are patch-like:
- with
includeSecrets=false, omitted gated fields preserve an existing local value; they remain absent on a newly created Stalker playlist, while a new Xtream entry follows the pending-credential flow above; - with
includeSecrets=true, a present field replaces the local value, an explicitly present empty string clears it, and an omitted field preserves an existing value. Provider-required fields such as an included Xtream username must still pass provider validation and cannot be cleared to an unusable value.
New Stalker backup matching uses normalized source URL, normalized MAC, profile
preset plus explicit identity fingerprint, and username when present, using the
same exact principal normalization defined for the session key. Password is
never part of a fingerprint. The legacy portalUrl + MAC fingerprint may match
a new entry only when it identifies exactly one existing playlist; an ambiguous
legacy match creates a separate restored entry rather than merging two
device/account profiles.
When credentials are excluded, backup entries that become identical only
because usernames were removed remain distinct through their exported entry
identity. A secret-stripped Stalker entry may preserve local credentials only
on an exact exportedId plus source/MAC/profile match; otherwise it creates a
separate credential-less entry. Restore must not deduplicate two
secret-stripped entries on the reduced fingerprint. exportedId remains
non-secret and must be non-empty and unique within the manifest; a duplicate is
rejected before any restore mutation.
Lazy Migration
There is no startup or bulk database migration.
- New Electron imports use the resolver by default.
- An existing full-portal playlist upgrades on first open or Test Connection.
- If
stalkerSourceUrlis absent, the existingportalUrlbecomes the source input; if that is absent, the legacyurlfield is used. - Learned fields and edited credentials are committed only after ready.
- A failed connection leaves the stored playlist unchanged.
isFullStalkerPortalremains a deprecated compatibility hint but no longer selects the auth algorithm; successful recipe classification derives its compatibility value for existing consumers.stalkerTokenis ignored immediately, is never written by the new path, and is removed when a playlist is successfully upgraded.- Existing explicit identity values are preserved; blank values are not generated.
- Stateless/simple portal records continue to use the current request path.
- PWA continues to use its existing adapter and existing regression tests.
On the next Electron open, full-session invokes the session manager and
stateless-mac invokes the simple adapter. For a legacy full or ambiguous
Electron record, a missing recipe triggers lazy reclassification. An explicitly
simple legacy record stays on the simple adapter unless the user invokes Test
Connection, edits its connection identity, or an endpoint-shape failure
requires reclassification. An older classifier version, changed source/endpoint
input, or a classified endpoint-shape failure also triggers reclassification
for an otherwise eligible Electron record. PWA continues to read the derived
legacy isFullStalkerPortal value until it gains its own request-recipe
contract.
There is no silent fallback from a failed new full-auth state machine to the known-incorrect legacy full-auth sequence. Failures remain typed and actionable. The unchanged simple-portal path is the compatibility path for portals that do not require a full session.
Error Taxonomy
Every failure has a stable reason, operation stage, retryability, and request ID. User-facing text is translated separately from the reason code.
| Signal | Outcome and action |
|---|---|
| Invalid URL, embedded basic auth, or unsupported scheme | invalid-url; stop before network |
| Invalid MAC format or oversized identity field | invalid-identity-input; stop before network |
| Redirect ceiling or repeated redirect target | redirect-loop; stop before an identity-bearing probe |
| No valid bounded candidate | endpoint-not-found; preserve input |
| Cross-origin challenge rejected or expired | origin-not-approved; stop before identity probe |
| DNS lookup failure | dns-failure; transient, keep stored state |
| Connection/network failure | network-unreachable; transient, keep stored state |
| Request timeout | request-timeout; transient, keep stored state |
| TLS validation/handshake failure | tls-failure; keep stored state and do not offer an insecure bypass |
Profile status 1 or recognized block field |
account-or-device-blocked; never cycle identity |
| Recognized device-ID/signature mismatch | device-identity-conflict; preserve configured identity for explicit correction |
Profile status 2 |
credentials-required challenge |
Explicitly failed do_auth or second profile still status 2 |
fresh credentials-required challenge; do not persist edited credentials |
| Three rejected credential submissions | credentials-attempt-limit; require a new attempt |
HTTP 401 or proven token rejection |
one serialized refresh and one operation retry |
| Refresh resolves a different principal | principal-transition-required; provisional reconnect, never in-place re-key |
| Rejection after that retry | auth-refresh-exhausted; stop without recursion |
HTTP-200 Access denied |
account-access-denied; no token refresh |
HTML/WAF 403 |
portal-protection-blocked; no token or identity rotation |
Learned endpoint returns 404, HTML, or wrong content type |
one secret-free rediscovery |
HTTP 429 |
rate-limited; stop candidate probes and honor a valid Retry-After |
HTTP 5xx |
portal-unavailable; no state mutation beyond the failed request |
| Response exceeds its operation cap | response-too-large; do not partially parse |
| Unsupported success/body/status shape | incompatible-response; preserve sanitized diagnostics |
| Atomic playlist write fails | local-persistence-failed; retain bounded provisional attempt for save retry |
| Runtime promotion fails after persistence | session-promotion-failed; discard runtime draft and reopen persisted config |
An operation has a budget of one rediscovery, one auth refresh, and one retry. Budgets are explicit state, not recursive calls.
Requests retain the existing validated redirect ceiling and finite timeouts.
Resolver/auth responses and catalog responses receive separate finite body-size
limits appropriate to their operation. A size limit produces
response-too-large; content is not partially parsed.
User Experience
The import and reconnection flow exposes these stages:
- Resolving portal
- Waiting for origin approval, when required
- Verifying device
- Credentials required, when required
- Validating credentials
- Saving connection
- Connected
The form stays open and retains its values and current safe discovery context after failure. Entering credentials continues the same trusted attempt rather than restarting discovery.
The origin prompt displays the exact source and final origins and explains that the MAC will be sent only after approval. The credential section is collapsed until requested, uses a masked password with a reveal control, and does not clear stored good credentials after a failed replacement.
Serial, device IDs, signatures, and profile overrides live in a collapsed advanced section. The UI does not imply that guessed values improve compatibility.
New imports are persisted only after ready. An existing playlist stays
unchanged after connection failure. Network ready is provisional: the UI first
writes the complete playlist draft atomically, then calls session commit, and
only then shows Connected.
A failed local write produces local-persistence-failed and offers Save Again
against the same provisional ready attempt for up to two minutes. It does not
claim that credentials or learned endpoint data were saved. Cancel, navigation,
or expiry calls discard, closes the provisional lease, and restores or
revalidates the previous ready session. commit is idempotent; if promotion
fails after a successful write, the UI returns session-promotion-failed,
discards the provisional runtime state, and may reopen from the now-persisted
record.
Stage 1 does not add a "save rejected credentials anyway" path.
Diagnostics and Privacy
Runtime diagnostics may contain:
- stage and stable reason code;
- recipe and profile preset IDs;
- HTTP status class and content type;
- redirect and candidate counts;
- bounded duration;
- request ID;
- a sanitized body-shape classification.
Diagnostics and logs must not contain:
- bearer token or handshake random;
- MAC or credential values;
- cookie names paired with values;
- serial, device IDs, or signatures;
- lease/challenge references;
- URL credentials or query secrets;
- raw response bodies;
- raw portal catalog metadata.
All Electron and renderer logs pass through the existing redacting portal/shared logging utilities. The fixture sanitizer is stricter than runtime log redaction; runtime redaction alone is not sufficient to publish a fixture.
Fixture and Replay Design
The existing catalog mock remains available. A separate stateful replay mode is
added to stalker-mock-server.
The server exposes an application factory so tests can start it on an ephemeral
port without importing a side-effectful main entry point.
A test creates an isolated replay run from a repository fixture and receives:
- an opaque
runId; - a synthetic entry URL;
- a run-generated, locally administered synthetic MAC and any explicit device fields required by the scenario;
- synthetic credentials when the scenario needs them.
The scenario identity is carried by the run URL. The generated MAC is only a protocol input and cannot select or share a scenario. Parallel tests have independent state.
Each fixture is deterministic JSON with:
- schema version and scenario ID;
- named virtual origins, where distinct aliases map to distinct ephemeral loopback listeners;
- entry/landing definition;
- ordered phases;
- request method and path;
- exact/present/absent query and header matchers;
- a discriminated request-body matcher;
- cookie attribute and presence matchers;
- an explicit response union;
- state transition and request cardinality;
- expected endpoint and terminal state;
- a fail-on-unexpected-request policy.
Every response has an integer status and a lower-case header map whose values
are always arrays. This preserves repeated Set-Cookie fields and represents
Location, Retry-After, and Content-Type without special lossy handling.
Its body is exactly one member of this discriminated union:
empty, for responses with no body;json, containing a schema-validated JSON value;jsonp, containing an allowlisted callback shape and JSON value;text, for bounded raw text or synthetic HTML;generated, for a bounded deterministic byte pattern used to cross a runtime response-size limit without committing a large blob.
The request-body matcher is exactly one of absent, json, form, or text.
JSON and form matchers support exact/present/absent fields and typed references;
text requires an exact bounded value. GET-based Stalker recipes declare
absent, so a redirect test cannot accidentally ignore identity or credential
fields moved from the query into a body.
Fixture values use typed symbol nodes. A generate node creates a safe,
run-scoped locally administered MAC, test credential, opaque token, random
value, cookie value, or request correlation value; a ref node requires the
exact previously generated value. Composite headers and cookies use a validated
parts node made only from fixed literals and typed references. Free-form
string interpolation and substring matching of secrets are forbidden. This
lets a fixture prove that a server-issued value is reused exactly without
storing a realistic static secret.
Concurrency is represented by a phase that permits explicitly bounded,
order-independent application requests. A deterministic barrier such as
releaseWhenMatched: 2 may hold matching responses until both original
operations have reached the token-rejection phase. Auth transitions remain
ordered. The ledger then proves that the manager created one refresh
generation and that each original operation was retried exactly once.
The replay ledger records only action names, counts, sanitized mismatch codes, safe fixture operation labels, and terminal state. It never records request secrets.
The run lifecycle is explicit:
createvalidates an allowlisted fixture and allocates isolated listeners, symbols, ledger, and expiry;- the test exercises the returned entry URL;
finalizefails if a cardinality is unmet, an unexpected request occurred, a barrier remains blocked, or the expected terminal state was not reached;disposecloses all listeners and erases the run even after test failure.
Fixture JSON is capped at 1 MiB, a scenario at 128 phases and 512 expectations, a run at 512 requests, and generated response data at 16 MiB. A run has a ten-minute hard lifetime and a two-minute inactivity timeout. These are schema-owned constants with boundary tests.
Unit/integration tests call the application factory in process. If an Electron E2E test needs an HTTP control plane, it is a separate loopback-only listener, validates the Host header, has no CORS support, accepts only repository allowlisted fixture IDs, caps control bodies at 64 KiB, and requires a random process-local capability for every create/finalize/dispose call. The capability is given only to the test harness and never appears in the synthetic portal URL.
Named origins provide real cross-origin semantics through different loopback
ports. Cookie rules that require HTTPS, registrable domains, public-suffix
checks, or an exact clock are tested against the injected cookie adapter using
synthetic https:// URLs and a fake clock; the replay server does not weaken
TLS verification or install a test CA.
Required Stage 1 scenario matrix
Unless a case is explicitly assigned to an injected transport, cookie-adapter, or fake-clock suite below, it is a committed replay fixture.
Resolver:
- root and
/c/; - custom-prefix
/c/; - direct
portal.phpandserver/load.php; - explicit stateless MAC classification after unsupported auth actions;
- an early stateless-capable candidate followed by a later full-session candidate, with full-session winning;
- an auth/protection failure after recorded stateless evidence, with downgrade forbidden;
- relative same-origin redirect;
- approved and rejected cross-origin redirect;
- redirect loop;
- HTML/
404candidate before a valid endpoint.
Cross-origin redirect security:
- landing, handshake, first/second profile,
do_auth, and catalog redirects each pause before the target listener receives an identity-bearing request; - rejection produces zero target-origin identity requests;
- approval restarts against the target origin with a fresh candidate jar, rather than forwarding source-origin MAC cookies, bearer authorization, credentials, optional identity, or identity query/body fields;
- a target
Locationcontaining user-info or a credential/auth query key is rejected before target traffic; - credentials are reused only if the target origin independently reaches
status
2.
Authentication:
- handshake with and without
random; - profile status numeric/string
0,1, and2; - recognized success without status;
- successful and failed
do_auth; - rejected saved credentials followed by a successful fresh challenge;
- three rejected submissions reaching the attempt limit;
- transport/WAF/rate-limit failure during
do_authremaining distinct from bad credentials; - second profile permitted only after successful
do_auth; store_auth_data_on_stbtrue and false without token persistence.
Classifier near misses:
do_authbodies containingjs: 1orjs: "true"producingincompatible-response, whilejs: falseis an explicit credential rejection and returns a fresh bounded challenge;- unknown profile status values and status-shaped fields in an unsupported
envelope producing
incompatible-response; - ambiguous
403bodies that match neither token rejection nor WAF evidence, producingincompatible-responsewithout refresh; - noncanonical variants of
Authorization failed, producingincompatible-responserather than a token rotation; - malformed JSONP, valid JSON labeled
text/htmlproducingincompatible-response, and an oversized generated response.
Identity and cookies:
- blank optional identity fields absent;
- explicit values forwarded unchanged;
- coherent profile headers/metrics;
- Set-Cookie, rotation, expiry, Secure, Domain, and Path;
- duplicate same-name cookies, redirect-hop cookies, and refresh rotation;
- attempted server shadowing of managed cookies through narrower Path/Domain;
- public-suffix Domain rejection and fake-clock expiry;
- failed-candidate jar poisoning prevention and atomic winning-jar promotion;
- isolation between two portal identities;
- two playlist leases with the same confirmed principal sharing one session;
- two usernames on the same endpoint/MAC/identity remaining isolated;
- alternating requests for those two principals switching under one base-identity coordinator without interleaved token generations;
- same-principal commit collision transferring existing leases atomically;
- an old-session refresh while a successful provisional attempt awaits save, followed by revalidation at promotion;
- failed provisional edit followed by restoration of the previous ready session;
- identity revision invalidating the jar.
Errors and refresh:
401;- token-auth
403; - WAF/HTML
403; 404and wrong content type;429andRetry-After;5xx;- HTTP-200
Authorization failed; - HTTP-200
Access denied; - refresh from username principal to
mac-only, requiring provisional replacement rather than in-place re-key; - refresh from
mac-onlyto status2, requiring a credential/reconnect flow; - concurrent failures joining exactly one refresh.
DNS failure, connection reset, timeout, and TLS failure use an injected transport with deterministic errors rather than pretending that a loopback HTTP fixture can reproduce those operating-system conditions.
Catalog pagination fixtures remain in the existing catalog scenario system.
HTTP 462 and stream-release fixtures are deferred to Stage 3.
Capture-to-Fixture Safety
A local developer tool may convert a HAR captured from a user-owned or controlled portal into a draft fixture. Raw captures must stay outside the workspace, git history, CI artifacts, and support attachments.
The converter:
- resolves and inspects the input with no-follow semantics, rejects symlinks and non-regular files, and rejects any raw input whose real path is inside a repository worktree;
- enforces raw-byte, decoded-byte, string, collection, and nesting-depth limits before materializing untrusted nested content;
- strictly decodes HAR
encoding: base64content or rejects it, and applies decoded-size limits before parsing the result; - structurally parses URLs, query parameters, headers, cookies, and JSON;
- replaces origins, MAC, credentials, tokens, cookies, serial/device/signature values, account IDs, artwork, stream URLs, and unique provider metadata with typed placeholders;
- preserves only protocol-relevant status, content type, redirect shape, cookie attributes, and body structure;
- scans raw, URL-encoded, double-encoded, and JSON-escaped forms;
- rejects unknown external origins, JWT/MAC/cookie literals, suspicious high-entropy strings, oversize values, invalid schema, and non-deterministic timestamps;
- exposes only stable sanitized error codes, never offending raw values;
- validates the complete generated fixture in memory before writing;
- emits deterministic, formatted JSON through an exclusive temporary regular file and atomic rename, rejecting a symlink or non-regular output target.
A second fail-closed validator runs in tests and CI over every committed fixture. Regex replacement of a few known values is insufficient.
Test Ownership
Unit and integration
portal-stalker-protocol: candidate recipes, profile normalization, pure transitions, response classifier, reserved parameters, and identity revision.stalker-mock-server: fixture schema, replay transitions, cardinality, run isolation, request-body matching, symbol binding, barriers, lifecycle finalization, resource caps, control-plane authorization, ledger sanitization, and unexpected-request failure.stalker-fixture-tools: structural capture conversion, deterministic formatting, input/output path safety, bounded/base64 parsing, placeholder validation, encoded-secret scans, entropy checks, and fail-closed behavior.electron-backend: cookie semantics, validated redirect integration, challenge binding/expiry, session leases, generation invalidation, principal isolation, base-identity read/write arbitration, same-principal lease transfer, provisional promotion/rollback and promotion-time revalidation, immutable-principal refresh transitions, single-flight refresh, response limits, and playback context. Direct boundary tests reject malformed or reserved IPC input and scan every returned DTO for token, random, cookie, credential, and identity leakage. Injected-transport tests cover DNS/network/timeout/TLS classification.electron-backendfake-clock suites prove watchdog activation, interval clamp/jitter, last-lease deactivation, renderer destruction, joining an in-flight refresh, and context invalidation. Playback contexts are bound to sender, session, generation, and exact stream URL; expire and are single-purpose; and become unusable after refresh, lease close, endpoint change, or identity change.portal-stalker-data-access: Electron facade outcomes, simple/PWA adapter preservation, migrated request routing, and absence of renderer token cache.portal-stalker-feature: route-level origin/credential dialogs, first-open lazy migration, request-time reconnection followed by one facade reissue, cancellation/navigation cleanup, and persistence retry.playlist-import-feature: progress states, origin approval, status-2 disclosure, successful credential persistence, failed credential preservation, and lazy migration.services: backup with structured credentials included/excluded, source URL, identity-secret gating, redacted Xtream conditional validation/pending state, patch-style merge semantics, version-1 compatibility, and round trip.web: backup export-choice presentation, residual-sensitivity warning, and secret-included/secret-excluded import behavior, including the redacted Xtream credential prompt and skip result.shared-logging: Stalker session payload and diagnostic redaction.
Electron E2E
Add atomized Electron E2E coverage:
stalker-auth.e2e.tscovers root/custom-path discovery, status2, cookie rotation, successful second profile, catalog request, and playback-context registration;- the same file covers parallel token rejection, one refresh, cookie/session isolation, and a successful retried request;
- the same file covers first open of an existing legacy full-portal playlist, route-level credential flow, saving, promotion, and reopen through the persisted recipe;
backup-roundtrip.e2e.tsproves the default secret exclusion, explicit secret inclusion, redacted Xtream prompt/skip behavior, patch-style restore semantics, and residual-sensitivity warning.
Keep the existing Electron provider smoke and PWA Stalker E2E as regression gates for stateless/simple behavior.
No real portal is used in CI.
Implementation Sequencing
The implementation plan should preserve these dependency-ordered slices:
- Evidence and pure contracts: fixture schema/validator, replay server support, protocol utility project, and state-machine/classifier tests. This slice changes no production connection behavior.
- Electron runtime: direct cookie-jar dependency, validated redirect integration, session manager, challenges, IPC/preload contracts, refresh, watchdog, and playback-context ownership.
- Renderer cutover: Angular facade, all full-portal call sites, import UX, lazy persistence, backup choice, removal of renderer token/header construction, Electron E2E, canonical docs, and release note.
No partial slice may advertise the new full-session behavior before its security-boundary and regression tests pass. The final cutover is atomic from a user perspective. Simple/PWA behavior remains available throughout.
portal-stalker-protocol, stalker-fixture-tools, and any new atomized test
targets named below are implementation deliverables, not assumed existing
projects. Their project.json files must include repository-standard
scope/domain/type tags. The implementation also classifies the new test
projects and newly test-enabled tool projects in
tools/coverage/coverage-policy.json and makes the replay validator's Nx inputs
include the committed fixture glob so fixture-only changes invalidate the
cache.
Acceptance Criteria
- A root or custom
/c/input resolves to the correct tested API endpoint. - A recognized stateless portal selects
stateless-mac, while auth rejection never falls back to that recipe. - A recorded stateless candidate never masks a later full-session candidate, and MIME/body near misses cannot become protocol success.
- A cross-origin landing sends no identity or secret before explicit approval.
- A cross-origin redirect from any identity-bearing operation never forwards source-origin session state and restarts only after explicit target approval.
- The winning handshake is reused rather than repeated.
- The first profile always carries
auth_second_step=0. do_authoccurs only after status2.- The second profile cannot occur before successful
do_authand carriesauth_second_step=1. - Ready is reached only after the appropriate final profile.
- Profile status and body errors handle numeric/string and documented envelope forms.
- Blank optional identity fields are absent and unrelated settings never rotate identity.
- Server cookies rotate and expire correctly and cannot leak across session identities.
- Same-MAC sessions for different principals never interleave auth generations; a stale provisional session is revalidated at promotion and same-principal leases survive atomic replacement.
- Token, random, cookies, and secret playback headers never cross the new Electron full-session preload boundary.
- IPC rejects reserved or malformed input; playback contexts are sender/session/generation/URL-bound and are invalidated on every owning session transition.
- Concurrent token failures perform one refresh and each operation retries at most once.
- Refresh never changes a committed principal in place; a changed discriminator goes through provisional persistence and promotion.
- Replay finalization rejects unmet cardinality, unexpected traffic, blocked concurrency barriers, nonterminal runs, and expired resource budgets.
Access denied, WAF403,429, and5xxdo not rotate identity or token.- New credentials are saved only after a successful second profile.
- Rejected saved credentials return a fresh bounded challenge, while transport, WAF, and rate-limit errors are never mislabeled as bad credentials.
- A failed lazy migration does not overwrite the playlist.
- Connection UI reaches Connected only after atomic playlist persistence and provisional-session promotion; failed saves are retryable and cleanly discardable.
stalkerTokenis not read or written by the new path.- Backup export excludes structured portal credentials by default and restores a credential-less Stalker entry through the normal status-2 flow.
- A redacted Xtream backup is schema-valid but never creates a credential-less playlist; it preserves an exact local match or waits for validated credentials.
- Existing simple Stalker and PWA regression tests continue to pass.
- All committed fixtures pass the fail-closed secret scanner.
Validation Ladder
Implementation validation must include the relevant Nx targets discovered at planning time. The expected minimum is:
pnpm nx test portal-stalker-protocol
pnpm nx test stalker-fixture-tools
pnpm nx test stalker-mock-server
pnpm nx test portal-stalker-data-access
pnpm nx test portal-stalker-feature
pnpm nx test playlist-import-feature
pnpm nx test services
pnpm nx test web
pnpm nx test shared-logging
pnpm nx test electron-backend
pnpm nx run electron-backend-e2e:e2e-ci--src/stalker-auth.e2e.ts
pnpm nx run electron-backend-e2e:e2e-ci--src/backup-roundtrip.e2e.ts
pnpm nx run electron-backend-e2e:e2e-ci--src/providers.e2e.ts
pnpm nx run web-e2e:e2e-ci--src/stalker.e2e.ts
Affected projects must also pass lint. The implementation plan must verify the exact target names with Nx before relying on this list.
Documentation and Release Note Impact
Implementation must update the existing canonical documents rather than leave the current renderer-owned session description stale:
docs/architecture/stalker-portal.md;docs/architecture/playlist-backup-restore.md;CLAUDE.mdif its Stalker, route, persistence, preload, or backup description is affected;AGENTS.mdonly if a process or ownership statement it contains changes;.codex/skills/stalker-portal/SKILL.mdif the session ownership checklist needs to guide future agents.
The runtime and UI behavior is user-visible, so implementation requires a
.changes/ release note and pnpm run release:notes:validate.
Follow-up Stages
Stage 2 can build on learned, non-secret portal capabilities to add:
- redacted connection diagnostics export;
- explicit incomplete-sync outcomes;
- adaptive pagination with repeated-page guards and accurate totals;
- transactional SQLite last-good catalog snapshots;
- optional FTS and stale-while-revalidate behavior.
Stage 3 begins with sanitized playback fixtures and traces:
create_link
→ redirects
→ effective headers/cookies
→ first byte or segment
→ HTTP 462 or success
→ previous-session release
Only behavior demonstrated by controlled fixtures should become an automatic recovery rule.