mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
docs(stalker): document smart endpoint discovery
This commit is contained in:
1 parent
d2d91e322b
commit
8d6fe1e02d
3 files changed
+144
-110
No files matched your search
@@ -4,8 +4,7 @@ area: stalker
|
||||
issues: [850, 686, 755]
|
||||
---
|
||||
|
||||
Stalker portals are no longer classified by their URL shape: importing probes
|
||||
the real API endpoint (`portal.php` vs `server/load.php`) and checks whether
|
||||
the portal actually requires authentication. Canonical Ministra URLs finally
|
||||
load content, `…/c` addresses resolve correctly, and misclassified existing
|
||||
portals repair themselves on first failure — keeping favorites and history.
|
||||
Stalker setup now accepts a host or `/c` address, finds the working API
|
||||
endpoint and authentication mode, and rechecks changed connection details in
|
||||
Edit. Misclassified portals from earlier versions still repair themselves on
|
||||
first compatible failure without losing favorites or history.
|
||||
@@ -1166,15 +1166,17 @@ engine` (restart required) or
|
||||
**Stalker Portal Mode and Endpoint Discovery**:
|
||||
|
||||
- Portal mode (full vs. simple) follows OBSERVED behavior, never a URL substring. The single predicate is `isFullStalkerPortalPlaylist()` / `isFullStalkerPortalUrl()` in `@iptvnator/shared/interfaces` (`stalker-portal-mode.util.ts`): the persisted `Playlist.isFullStalkerPortal` flag is authoritative and the URL shape is a fallback for legacy rows only. Three diverging copies of this rule used to exist and shipped broken configurations (#850/#686/#755) — never re-implement it. A token-enforcing `portal.php` panel is a full portal; a `server/load.php` endpoint that answers without a token is a simple one.
|
||||
- Import probes candidates in order (a pasted `.php` endpoint first, then `<base>/portal.php` → `<base>/server/load.php` → `<base>/stalker_portal/server/load.php`) and classifies each by behavior — a token-less `itv/get_genres` returning data proves a token-free panel; the plain-text auth failure proves a full portal, confirmed by a real handshake + `get_profile`. `StalkerPortalDiscoveryService` (`libs/portal/stalker/data-access`) persists the proven endpoint and mode.
|
||||
- `executeStalkerRequest()` (`stores/utils/stalker-request.utils.ts`) is the choke point for catalog, content and playback requests: mode routing, the in-session repair override, and retry-once all live there. Four callers are deliberately outside it because they run below or before the thing it routes on — `StalkerAuthApi` (handshake/`get_profile`/`do_auth`, which the full-portal branch is built from; routing them back would recurse), `StalkerPortalDiscoveryService` (probes precede the mode they determine), `StalkerAccountInfoService.fetchViaProfile()`, and `StreamResolverService` for a collection item with no playlist row. They are exempt from the routing, not from the repair it hooks, but only `fetchViaProfile()` wires `StalkerPortalRepairService` itself: discovery is what repair *drives*, the row-less resolver branch has no playlist to repair, and the auth layer needs nothing — a terminal handshake failure propagates out of the full-portal branch into whichever `executeStalkerRequest()` call triggered the authentication, which is why terminal handshake failures are a repair trigger. Anything new that is not auth or discovery belongs on `executeStalkerRequest()`. Existing playlists are repaired LAZILY (`StalkerPortalRepairService`) — only after a request fails with a shape a wrong endpoint/mode produces, at most once per source configuration per playlist per session, persisted through the atomic `PlaylistsService.transformPlaylistMeta`. There is deliberately **no eager one-shot migration**: a portal that works is never re-probed.
|
||||
- Import requires an explicit HTTP(S) scheme but accepts a bare host, `/c`, or a concrete `.php` address. It probes candidates in order (a pasted `.php` endpoint first, then `<base>/portal.php` → `<base>/server/load.php` → `<base>/stalker_portal/server/load.php`) and classifies each by behavior — a token-less `itv/get_genres` returning data proves a token-free panel; the plain-text auth failure proves a full portal, confirmed by a real handshake + `get_profile`. `StalkerPortalDiscoveryService` (`libs/portal/stalker/data-access`) persists and displays the proven endpoint and mode. An unreachable panel-style import remains allowed with a warning; a bare host falls back to `<base>/portal.php`, while canonical-shaped unreachable addresses still abort.
|
||||
- The playlist-info Edit dialog preserves an unchanged connection byte-for-byte and skips discovery. Changing URL, MAC, credentials, serial, device IDs or signatures blocks duplicate saves and runs the existing discovery service through the app-provided `STALKER_PLAYLIST_CONNECTION_EDITOR` token, keeping Stalker data-access out of `playlist-shared-ui`. Failure writes nothing; success atomically replaces endpoint, mode, normalized identity and session metadata, then synchronizes the active `StalkerStore` snapshot and session/watchdog before another same-route request can use the old connection. The transient `PlaylistMetaUpdate.stalkerSessionPatch` preserves on absence, clears on `null`, and fully replaces from an object before storage; it is projected onto existing flat playlist fields and never changes the DB or backup shape.
|
||||
- `executeStalkerRequest()` (`stores/utils/stalker-request.utils.ts`) is the choke point for catalog, content and playback requests: mode routing, the in-session repair override, and retry-once all live there. Four callers are deliberately outside it because they run below or before the thing it routes on — `StalkerAuthApi` (handshake/`get_profile`/`do_auth`, which the full-portal branch is built from; routing them back would recurse), `StalkerPortalDiscoveryService` (probes precede the mode they determine), `StalkerAccountInfoService.fetchViaProfile()`, and `StreamResolverService` for a collection item with no playlist row. They are exempt from the routing, not from the repair it hooks, but only `fetchViaProfile()` wires `StalkerPortalRepairService` itself: discovery is what repair _drives_, the row-less resolver branch has no playlist to repair, and the auth layer needs nothing — a terminal handshake failure propagates out of the full-portal branch into whichever `executeStalkerRequest()` call triggered the authentication, which is why terminal handshake failures are a repair trigger. Anything new that is not auth or discovery belongs on `executeStalkerRequest()`. Existing playlists are repaired LAZILY (`StalkerPortalRepairService`) — only after a request fails with a shape a wrong endpoint/mode produces, at most once per source configuration per playlist per session, persisted through the atomic `PlaylistsService.transformPlaylistMeta`. There is deliberately **no eager one-shot migration**: a portal that works is never re-probed.
|
||||
- Explicit Edit advances the repair generation before installing its resolved session. A lazy repair that started earlier is discarded even if it had already verified its row, so it cannot restore an older endpoint, mode or token after Edit.
|
||||
- Both transports build the wire format from the same shared builders in `@iptvnator/shared/interfaces` — `buildStalkerRequestUrl()`, `buildStalkerIdentityRequestContext()`, `encodeStalkerCmdValue()` — so the Electron and PWA legs cannot drift. The mock's `/stalker` mirror shares the identity builder only — it dispatches in-process, so there is no portal URL to build and it mirrors the `JsHttpRequest` default by hand. Never fork any of them.
|
||||
- Simple portals skip the auth lifecycle (no handshake, token or watchdog) but their requests are not stripped to a bare cookie: they still carry everything the shared builder derives from a MAC alone (`mac`/`stb_lang`/`timezone` cookie, MAG `User-Agent`/`X-User-Agent`, `Accept` set). They do NOT carry the serial — `dispatchStalkerRequest()`'s direct branch forwards only `url`/`macAddress`/`params`, so no `SN` header and no serial-derived `__cfduid`, whatever the playlist stores. That gate is on API requests only: `buildStalkerExternalPlaybackHeaders()` reads the serial off the playlist row with no mode check, so the same simple-mode playlist does send `SN`/`__cfduid` with a portal-owned stream.
|
||||
- Contract: `docs/architecture/stalker-portal.md` ("Portal Mode and Endpoint Discovery", "Request Transport and `cmd` Encoding").
|
||||
|
||||
**Stalker Session Authentication**:
|
||||
|
||||
- Full portals authenticate through `StalkerSessionService` (`libs/portal/stalker/data-access/src/lib/stalker-session.service.ts`), a thin facade over `stalker-auth.api.ts` (handshake / `get_profile` / `do_auth` + the `authenticate()` orchestration), `stalker-watchdog.controller.ts`, `stalker-token-cache.ts` (in-run token + pending-auth state, tagged with the identity fingerprint), `stalker-session-store.ts` (the session persisted on the playlist row), `stalker-portal-error.ts` and `stalker-response-classification.ts`.
|
||||
- Full portals authenticate through `StalkerSessionService` (`libs/portal/stalker/data-access/src/lib/stalker-session.service.ts`), a thin facade over `stalker-auth.api.ts` (handshake / `get_profile` / `do_auth` + the `authenticate()` orchestration), `stalker-authenticated-request-client.ts`, `stalker-edited-session-coordinator.ts` (authoritative Edit/session serialization), `stalker-watchdog.controller.ts`, `stalker-token-cache.ts` (in-run token + pending-auth state, tagged with the identity fingerprint), `stalker-session-store.ts` (the session persisted on the playlist row), `stalker-portal-error.ts` and `stalker-response-classification.ts`.
|
||||
- `get_profile`'s `js.status` decodes as: full profile/`0` = OK, `1` = refused (`device-conflict` when the message says so, otherwise `blocked`), `2` = login/password required → `do_auth` then `get_profile` with `auth_second_step=1` (only that retry sets it). A bare `{status: 1}` with no message is a refusal, not a success. Credentials come from the import dialog's username/password fields and are persisted so runtime re-auth can repeat `do_auth`. Status is read through a numeric coercion — portals stringify it.
|
||||
- Refusals throw `StalkerPortalError` (`login-required` / `login-rejected` / `device-conflict` / `blocked` / `auth-failed`) carrying the portal's markup-stripped `msg`/`block_msg` in `portalText`; the import dialog and the workspace context panel render it. Read it with `asStalkerPortalError()`, never `instanceof` in lazy-loaded code. `device-conflict` splits off `blocked` via `isStalkerDeviceConflictMessage` (narrow phrase set, structured `msg` only): it is the one refusal with a remedy, and the portal's own "Your STB is damaged" wording points away from it, so both surfaces lead with their own headline and append the portal text.
|
||||
- Auth failures are HTTP 200 + plain text (`Authorization failed.` / `Access denied.` / `Unauthorized request.`), classified at the transport boundary by `libs/shared/interfaces/src/lib/stalker-auth-failure.util.ts`; the Electron handler **returns** a `{stalkerAuthFailure}` marker rather than throwing, because `ipcRenderer.invoke` strips custom properties off rejections.
|
||||
|
||||
+135
-102
@@ -60,41 +60,42 @@ below is reached as `/workspace/stalker/:id/…`.
|
||||
`DataService.sendIpcEvent(STALKER_REQUEST, ...)` directly. It also hooks
|
||||
the lazy portal repair (see "Portal Mode and Endpoint Discovery").
|
||||
|
||||
Four callers deliberately sit outside it and issue `STALKER_REQUEST`
|
||||
themselves, because each one runs *below* or *before* what it routes on:
|
||||
Four callers deliberately sit outside it and issue `STALKER_REQUEST`
|
||||
themselves, because each one runs _below_ or _before_ what it routes on:
|
||||
|
||||
- `StalkerAuthApi` — `handshake` / `get_profile` / `do_auth` are what the
|
||||
full-portal branch is implemented in terms of, so routing them back
|
||||
through it would recurse.
|
||||
- `StalkerPortalDiscoveryService` — probes run before a mode exists; the
|
||||
mode is what they are determining.
|
||||
- `StalkerAccountInfoService.fetchViaProfile()` — the full-mode refresh is
|
||||
a profile request, so it takes the same exemption as the auth layer.
|
||||
- `StreamResolverService`, for a collection item carrying its own portal
|
||||
coordinates with **no playlist row** — there is no meta to route or
|
||||
repair with. The playlist-backed branch beside it does use
|
||||
`executeStalkerRequest()`, and wins when a row exists, so a repaired
|
||||
endpoint beats a stale favorite's snapshot.
|
||||
- `StalkerAuthApi` — `handshake` / `get_profile` / `do_auth` are what the
|
||||
full-portal branch is implemented in terms of, so routing them back
|
||||
through it would recurse.
|
||||
- `StalkerPortalDiscoveryService` — probes run before a mode exists; the
|
||||
mode is what they are determining.
|
||||
- `StalkerAccountInfoService.fetchViaProfile()` — the full-mode refresh is
|
||||
a profile request, so it takes the same exemption as the auth layer.
|
||||
- `StreamResolverService`, for a collection item carrying its own portal
|
||||
coordinates with **no playlist row** — there is no meta to route or
|
||||
repair with. The playlist-backed branch beside it does use
|
||||
`executeStalkerRequest()`, and wins when a row exists, so a repaired
|
||||
endpoint beats a stale favorite's snapshot.
|
||||
|
||||
The exemption is from the routing, not from the repair it hooks — but only
|
||||
**one** of the four wires `StalkerPortalRepairService` itself, and the
|
||||
asymmetry is worth knowing before adding a fifth:
|
||||
The exemption is from the routing, not from the repair it hooks — but only
|
||||
**one** of the four wires `StalkerPortalRepairService` itself, and the
|
||||
asymmetry is worth knowing before adding a fifth:
|
||||
|
||||
- `StalkerAccountInfoService.fetchViaProfile()` wires it explicitly,
|
||||
because opening the account dialog on a playlist with a stale endpoint
|
||||
must be able to fix it instead of waiting for an unrelated catalog
|
||||
request.
|
||||
- `StalkerPortalDiscoveryService` is what repair *drives*, so it cannot
|
||||
repair itself.
|
||||
- The auth layer wires nothing. It does not need to: a terminal handshake
|
||||
failure propagates out of the full-portal branch and is caught by
|
||||
whichever `executeStalkerRequest()` call triggered the authentication,
|
||||
which is exactly why "terminal handshake failures" is one of the repair
|
||||
triggers listed above.
|
||||
- `StreamResolverService`'s row-less branch has no playlist to repair.
|
||||
- `StalkerAccountInfoService.fetchViaProfile()` wires it explicitly,
|
||||
because opening the account dialog on a playlist with a stale endpoint
|
||||
must be able to fix it instead of waiting for an unrelated catalog
|
||||
request.
|
||||
- `StalkerPortalDiscoveryService` is what repair _drives_, so it cannot
|
||||
repair itself.
|
||||
- The auth layer wires nothing. It does not need to: a terminal handshake
|
||||
failure propagates out of the full-portal branch and is caught by
|
||||
whichever `executeStalkerRequest()` call triggered the authentication,
|
||||
which is exactly why "terminal handshake failures" is one of the repair
|
||||
triggers listed above.
|
||||
- `StreamResolverService`'s row-less branch has no playlist to repair.
|
||||
|
||||
Anything new that is not auth or discovery belongs on
|
||||
`executeStalkerRequest()`.
|
||||
|
||||
Anything new that is not auth or discovery belongs on
|
||||
`executeStalkerRequest()`.
|
||||
4. Electron main process handles `STALKER_REQUEST` in
|
||||
`apps/electron-backend/src/app/events/stalker.events.ts`.
|
||||
5. Axios calls the portal's persisted API endpoint (`portal.php` on
|
||||
@@ -135,20 +136,20 @@ Two portal modes exist, persisted per playlist as
|
||||
`Accept`/`Accept-Language`/`Connection` set (see "Request Transport and
|
||||
`cmd` Encoding").
|
||||
|
||||
What it does **not** get is anything carried by the session. The direct
|
||||
branch of `dispatchStalkerRequest()` forwards only `url`, `macAddress` and
|
||||
`params`, so the builder never sees a token *or* a serial: no
|
||||
`Authorization: Bearer` (there is none to send), and no `SN` header or
|
||||
serial-derived `__cfduid` cookie **even when the playlist stores a serial**.
|
||||
The full-portal branch forwards both, because `makeAuthenticatedRequest()`
|
||||
passes `token` and `serialNumber`. This covers portal API requests only —
|
||||
playback headers are built from the playlist row by a different helper and
|
||||
are NOT mode-gated, so the same simple-mode playlist does send its serial
|
||||
with a portal-owned stream (see "Stalker Identity Policy").
|
||||
Whether a simple panel ought to receive
|
||||
the serial is unproven: no reference portal is known to require it, and the
|
||||
`sn` *parameter* only ever travels on `get_profile`, which a simple portal
|
||||
never calls. Treat it as an open question rather than a bug to fix blind.
|
||||
What it does **not** get is anything carried by the session. The direct
|
||||
branch of `dispatchStalkerRequest()` forwards only `url`, `macAddress` and
|
||||
`params`, so the builder never sees a token _or_ a serial: no
|
||||
`Authorization: Bearer` (there is none to send), and no `SN` header or
|
||||
serial-derived `__cfduid` cookie **even when the playlist stores a serial**.
|
||||
The full-portal branch forwards both, because `makeAuthenticatedRequest()`
|
||||
passes `token` and `serialNumber`. This covers portal API requests only —
|
||||
playback headers are built from the playlist row by a different helper and
|
||||
are NOT mode-gated, so the same simple-mode playlist does send its serial
|
||||
with a portal-owned stream (see "Stalker Identity Policy").
|
||||
Whether a simple panel ought to receive
|
||||
the serial is unproven: no reference portal is known to require it, and the
|
||||
`sn` _parameter_ only ever travels on `get_profile`, which a simple portal
|
||||
never calls. Treat it as an open question rather than a bug to fix blind.
|
||||
|
||||
Neither label is tied to a URL shape: mode follows OBSERVED behavior, so a
|
||||
`portal.php` panel that enforces the token is classified — and treated
|
||||
@@ -164,31 +165,58 @@ copies of this rule existed (import, session service, legacy-flag migration)
|
||||
and their drift shipped broken configurations (#850, #686, #755); no new
|
||||
consumer may re-implement the rule.
|
||||
|
||||
**Endpoint discovery (import).** `portal.php` does not exist in official
|
||||
Stalker/Ministra — it is a reseller-panel alias; the canonical endpoint
|
||||
derived from a `…/c` URL is `<base>/server/load.php`. Instead of guessing
|
||||
from the URL shape, `StalkerPortalDiscoveryService`
|
||||
(`libs/portal/stalker/data-access`) probes candidates in order — the pasted
|
||||
URL itself when it already names a `.php` endpoint, then `<base>/portal.php`
|
||||
→ `<base>/server/load.php` → `<base>/stalker_portal/server/load.php` — and
|
||||
classifies each endpoint by observed behavior: a token-less
|
||||
**Endpoint discovery (import and edit).** The address field requires an
|
||||
explicit `http://` or `https://` scheme, but accepts a bare host, a browser
|
||||
entry point such as `…/c`, or a concrete `.php` API endpoint. `portal.php`
|
||||
does not exist in official Stalker/Ministra — it is a reseller-panel alias;
|
||||
the canonical endpoint derived from a `…/c` URL is
|
||||
`<base>/server/load.php`. Instead of guessing from the URL shape,
|
||||
`StalkerPortalDiscoveryService` (`libs/portal/stalker/data-access`) probes
|
||||
candidates in order — the pasted URL itself when it already names a `.php`
|
||||
endpoint, then `<base>/portal.php` → `<base>/server/load.php` →
|
||||
`<base>/stalker_portal/server/load.php` — and classifies each endpoint by
|
||||
observed behavior: a token-less
|
||||
`itv/get_genres` that returns data proves a token-free panel; the plain-text
|
||||
auth failure proves the endpoint enforces the token, which is confirmed by
|
||||
running the real handshake + `get_profile`. The import dialog persists the
|
||||
proven endpoint and mode. When no candidate answers at all, panel-style URLs
|
||||
fall back to the pre-discovery behavior (legacy `…/c` → `portal.php` rewrite,
|
||||
simple mode, import succeeds with a warning) so temporarily offline panels
|
||||
can still be added; canonical-shaped URLs abort like the old mandatory
|
||||
handshake did (both classifications run on the normalized
|
||||
`origin + pathname` form). Probe failure sequencing: ANY resolvable HTTP
|
||||
status moves to the next candidate — 4xx means the endpoint is absent, a
|
||||
5xx can be one broken handler beside a healthy sibling — and 401/403
|
||||
specifically classify as auth-required (the handshake is attempted, for
|
||||
middlewares that answer HTTP auth codes instead of the stock 200 +
|
||||
proven endpoint and mode, and that resolved API endpoint is what Edit shows.
|
||||
When no candidate answers at all, panel-style URLs fall back to the
|
||||
pre-discovery behavior (a bare host becomes `<base>/portal.php`; legacy
|
||||
`…/c` becomes the matching `portal.php`; simple mode, import succeeds with a
|
||||
warning) so temporarily offline panels can still be added. Canonical-shaped
|
||||
URLs abort like the old mandatory handshake did (both classifications run on
|
||||
the normalized `origin + pathname` form). Probe failure sequencing: ANY
|
||||
resolvable HTTP status moves to the next candidate — 4xx means the endpoint
|
||||
is absent, a 5xx can be one broken handler beside a healthy sibling — and
|
||||
401/403 specifically classify as auth-required (the handshake is attempted,
|
||||
for middlewares that answer HTTP auth codes instead of the stock 200 +
|
||||
plain-text body). Status-less TIMEOUTS also continue (a single handler can
|
||||
hang); only connection-level failures (refused, unresolvable host) abort
|
||||
discovery, since every candidate shares the host.
|
||||
|
||||
The playlist-info Edit dialog compares URL, MAC, username/password, serial,
|
||||
device IDs and signatures as one connection identity. A metadata-only edit
|
||||
does not run discovery and preserves the stored connection byte-for-byte.
|
||||
Changing any connection field disables the form while the same discovery
|
||||
service validates the draft. Auth rejection or an unreachable portal leaves
|
||||
the dialog open and writes nothing. Success atomically replaces the endpoint,
|
||||
mode and normalized identity together with session metadata: simple mode
|
||||
clears token/fingerprint/watchdog/account state, while full mode replaces it
|
||||
with the confirmed authorization result. The app adapter also replaces the
|
||||
active `StalkerStore` snapshot and session/watchdog state immediately, so the
|
||||
next request in the same route cannot use the pre-edit connection.
|
||||
`PlaylistMetaUpdate` carries the persisted part as a transient
|
||||
`stalkerSessionPatch` (`undefined` preserves, `null` clears, an object fully
|
||||
replaces); `PlaylistsService` projects it onto the existing flat playlist
|
||||
fields, so the patch itself never enters SQLite, IndexedDB or a backup and no
|
||||
schema or backup-version migration is required.
|
||||
|
||||
The shared playlist UI exposes only
|
||||
`STALKER_PLAYLIST_CONNECTION_EDITOR` and its
|
||||
`resolved | auth-rejected | unreachable` result contract. The web composition
|
||||
layer implements the token with Stalker data-access; the UI library must not
|
||||
import Stalker discovery directly.
|
||||
|
||||
**Lazy repair (existing playlists).** The flag is frozen in the DB, so
|
||||
records persisted by the old guess stay broken without repair — but a large
|
||||
share of users are on working reseller panels, and only probing can tell the
|
||||
@@ -209,7 +237,10 @@ keep working) and persisted through `PlaylistsService.transformPlaylistMeta`
|
||||
queue, so a user edit that is queued but not yet committed wins over the
|
||||
repair instead of being overwritten; the transform patches the freshly read
|
||||
row (`portalUrl` + `isFullStalkerPortal` only, so user state can never be
|
||||
clobbered) and returns null to abort. Deletion runs through the same queue,
|
||||
clobbered) and returns null to abort. Explicit Edit also advances an in-run
|
||||
generation before replacing the session, so a repair that already verified
|
||||
its row but finishes later cannot install its older override or token.
|
||||
Deletion runs through the same queue,
|
||||
so a repair can never resurrect a playlist deleted mid-probe. Portals
|
||||
that work are never probed, let alone rewritten. E2E coverage:
|
||||
`apps/electron-backend-e2e/src/stalker-portal-discovery.e2e.ts` against the
|
||||
@@ -288,8 +319,8 @@ describes the emulated box, not the account — see "Reported device profile".)
|
||||
and reused for initial auth, token refresh, retry auth, normal API requests,
|
||||
and same-origin playback headers.
|
||||
|
||||
Those last two reach the wire by different routes, and **only one is
|
||||
mode-gated** — the asymmetry is easy to get backwards:
|
||||
Those last two reach the wire by different routes, and **only one is
|
||||
mode-gated** — the asymmetry is easy to get backwards:
|
||||
|
||||
- **Portal API requests** receive the serial through
|
||||
`makeAuthenticatedRequest()`, which only the full-portal branch of
|
||||
@@ -299,9 +330,10 @@ describes the emulated box, not the account — see "Reported device profile".)
|
||||
- **Same-origin playback headers** are built by
|
||||
`buildStalkerExternalPlaybackHeaders()`, which reads
|
||||
`playlist.stalkerSerialNumber` straight off the row with no mode check
|
||||
at all. So a simple-mode playlist holding a real serial *does* send `SN`
|
||||
at all. So a simple-mode playlist holding a real serial _does_ send `SN`
|
||||
and the serial-derived `__cfduid` on portal-owned streams — while its
|
||||
API calls do not.
|
||||
|
||||
- Empty optional identity fields remain absent. IPTVnator must never generate a
|
||||
device ID behind the user's back, and must never duplicate `device_id2` from
|
||||
`device_id1` on its own.
|
||||
@@ -343,7 +375,7 @@ never rewritten on read:
|
||||
- rewriting them at the transport would move `stalkerSessionFingerprint` for
|
||||
every existing playlist at once, with no user action and no way to opt out.
|
||||
|
||||
An edit therefore *does* move the fingerprint and force a re-authentication.
|
||||
An edit therefore _does_ move the fingerprint and force a re-authentication.
|
||||
That is intended: the user changed the identity, and the field shows exactly
|
||||
what will be sent.
|
||||
|
||||
@@ -357,7 +389,7 @@ MAC. Refusing one would stop those users adding or editing a portal that works
|
||||
for them. The mock encodes the same split (`enforceMacFormat` is set only on
|
||||
the strict endpoint; `/portal.php` ignores it), and `AUTH_REJECTED_MAC` in
|
||||
`stalker.e2e.ts` depends on it — a non-Infomir MAC that must reach the strict
|
||||
endpoint and be refused *there*, not in the form.
|
||||
endpoint and be refused _there_, not in the form.
|
||||
|
||||
In the edit dialog **both** passes — blur and submit — normalize only a MAC the
|
||||
user actually changed, compared against the value the dialog was opened with
|
||||
@@ -425,10 +457,11 @@ later empty value as a permanent lockout. Therefore:
|
||||
deliberately emptied, and the import would pin IDs the user opted out
|
||||
of.
|
||||
|
||||
All three are mutation-verified. Note the tests have to control when the
|
||||
digest settles (hold it behind a gate, or delay the older invocation):
|
||||
Node resolves digests this small in start order, so the naive versions pass
|
||||
with the guards removed;
|
||||
All three are mutation-verified. Note the tests have to control when the
|
||||
digest settles (hold it behind a gate, or delay the older invocation):
|
||||
Node resolves digests this small in start order, so the naive versions pass
|
||||
with the guards removed;
|
||||
|
||||
- **the MAC and its device IDs travel as one value.**
|
||||
`settleMacAddressIdentity()` reads the MAC once, before it awaits, and
|
||||
returns it together with the IDs derived from exactly it; `addPlaylist()`
|
||||
@@ -459,17 +492,17 @@ later empty value as a permanent lockout. Therefore:
|
||||
out" would be false and would discourage them from fixing an ID that was
|
||||
never pinned.
|
||||
|
||||
**Known imprecision, deliberately not closed here.** The gate proves that
|
||||
*some* device ID reached the portal, not that the currently stored one did:
|
||||
a user who edits the ID after import keeps `isFullStalkerPortal` true while
|
||||
the new value has never seen `get_profile`. The copy is hedged for exactly
|
||||
that ("a device ID", not "this one") and the fields stay editable, so the
|
||||
remedy — restoring the ID that was pinned — is never blocked. Making it
|
||||
exact needs a per-identity confirmation signal;
|
||||
`Playlist.stalkerSessionIdentity` already carries a session fingerprint that
|
||||
would serve, but reading it here would make a `type:ui` playlist library
|
||||
depend on the Stalker data-access lib, which the Nx boundaries forbid. Worth
|
||||
revisiting behind a shared contract, not worth a boundary exception.
|
||||
**Known imprecision, deliberately not closed here.** The gate proves that
|
||||
_some_ device ID reached the portal, not that the currently stored one did:
|
||||
a user who edits the ID after import keeps `isFullStalkerPortal` true while
|
||||
the new value has never seen `get_profile`. The copy is hedged for exactly
|
||||
that ("a device ID", not "this one") and the fields stay editable, so the
|
||||
remedy — restoring the ID that was pinned — is never blocked. Making it
|
||||
exact needs a per-identity confirmation signal;
|
||||
`Playlist.stalkerSessionIdentity` already carries a session fingerprint that
|
||||
would serve, but reading it here would make a `type:ui` playlist library
|
||||
depend on the Stalker data-access lib, which the Nx boundaries forbid. Worth
|
||||
revisiting behind a shared contract, not worth a boundary exception.
|
||||
|
||||
The trade-off the option exists for is interoperability, not obfuscation: a
|
||||
user who reaches the same account from StbEmu already has this exact value
|
||||
@@ -553,7 +586,7 @@ imported before the cadence was persisted has a reusable token and no
|
||||
cadence, and skipping would strand it on the 120 s default permanently, since
|
||||
the profile is the only thing that could teach it. Such a playlist runs one
|
||||
profile, persists what it learns, and skips from then on. What gets persisted
|
||||
is the *effective* cadence (the 120 s default when the portal advertises
|
||||
is the _effective_ cadence (the 120 s default when the portal advertises
|
||||
none), so stored absence keeps meaning exactly one thing — never profiled —
|
||||
rather than sending a portal that advertises nothing back through a profile on
|
||||
every start.
|
||||
@@ -785,15 +818,15 @@ portal has to resolve the command. `null` is deliberately wider than the flag
|
||||
check alone; the extra guards can only push a row back onto the `create_link`
|
||||
path, so they cannot regress a portal that works today:
|
||||
|
||||
| Input | Verdict | Why |
|
||||
| --- | --- | --- |
|
||||
| Either flag truthy (`1`, `'1'`, `true`) | `create_link` | The portal asked for a temporary link. |
|
||||
| No row supplied at all | `create_link` | A caller that cannot show the flags gets no verdict. |
|
||||
| A row carrying neither flag KEY | `create_link` | Absence means "no evidence", not "no". A stock portal returns both flags on every row, so their PRESENCE is the provenance signal — and the only one available, because rows persisted into Favorites/Recently Viewed before this change were stripped of them by `buildStalkerSelectedVodItem`'s whitelist, making a legacy snapshot indistinguishable from a genuinely unflagged row. There is no migration for those. **Radio is the documented exception**: a directly playable radio command has always played as-is, so a flagless radio row keeps that rather than newly minting. |
|
||||
| Relative `/media/file_12.mpg` or query-only `?token=…` | `create_link` | Only the portal turns those into an address; the VOD `has_files` rewrite produces exactly the first shape. |
|
||||
| Non-HTTP scheme (`ffrt4://ch/live/…`) | `create_link` | Portal-internal pseudo-URL. |
|
||||
| Portal-local host — `localhost` and any `*.localhost` name (RFC 6761 §6.3 reserves the whole suffix for loopback), `localhost.localdomain`, all of `127.0.0.0/8`, `0.0.0.0`, `::1`, `::`, and the IPv4-mapped forms `URL` normalizes to hex (`::ffff:7f00:1`); a terminal DNS root dot is stripped first | `create_link` | `ffrt3 http://localhost/ch/1234_` is an instruction to the portal, not an address a set-top box could open. |
|
||||
| Otherwise | static `cmd` | Solution prefix stripped by `normalizeStalkerPlaybackCommand()`. |
|
||||
| Input | Verdict | Why |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Either flag truthy (`1`, `'1'`, `true`) | `create_link` | The portal asked for a temporary link. |
|
||||
| No row supplied at all | `create_link` | A caller that cannot show the flags gets no verdict. |
|
||||
| A row carrying neither flag KEY | `create_link` | Absence means "no evidence", not "no". A stock portal returns both flags on every row, so their PRESENCE is the provenance signal — and the only one available, because rows persisted into Favorites/Recently Viewed before this change were stripped of them by `buildStalkerSelectedVodItem`'s whitelist, making a legacy snapshot indistinguishable from a genuinely unflagged row. There is no migration for those. **Radio is the documented exception**: a directly playable radio command has always played as-is, so a flagless radio row keeps that rather than newly minting. |
|
||||
| Relative `/media/file_12.mpg` or query-only `?token=…` | `create_link` | Only the portal turns those into an address; the VOD `has_files` rewrite produces exactly the first shape. |
|
||||
| Non-HTTP scheme (`ffrt4://ch/live/…`) | `create_link` | Portal-internal pseudo-URL. |
|
||||
| Portal-local host — `localhost` and any `*.localhost` name (RFC 6761 §6.3 reserves the whole suffix for loopback), `localhost.localdomain`, all of `127.0.0.0/8`, `0.0.0.0`, `::1`, `::`, and the IPv4-mapped forms `URL` normalizes to hex (`::ffff:7f00:1`); a terminal DNS root dot is stripped first | `create_link` | `ffrt3 http://localhost/ch/1234_` is an instruction to the portal, not an address a set-top box could open. |
|
||||
| Otherwise | static `cmd` | Solution prefix stripped by `normalizeStalkerPlaybackCommand()`. |
|
||||
|
||||
`fetchStalkerPlaybackLink()` applies the verdict for ITV, VOD and radio, and
|
||||
short-circuits before the request. It never applies it when `series` is set:
|
||||
@@ -915,14 +948,14 @@ A temporary link lives about 5 seconds (`tv_tmp_link_ttl` /
|
||||
so the rule is to resolve immediately before playback and never persist,
|
||||
cache or replay the result. What each persisting path actually stores:
|
||||
|
||||
| Path | Stores | Verdict |
|
||||
| --- | --- | --- |
|
||||
| Recently Viewed | the raw row including `cmd` (`buildStalkerRecentlyViewedPayload` spreads the item) | Re-resolves on replay. |
|
||||
| Favorites | the raw row including `cmd` (`addStalkerFavorite`, `toggleFavorite`) | Re-resolves on replay. |
|
||||
| Playback positions | `playlist_id` + `content_xtream_id` + content type only — no URL column | Not applicable. |
|
||||
| Main-process playback context (`stalker-playback-context.service.ts`) | header sets keyed by the stream URL's origin + path, 15 min TTL | Stores no URL. The key drops the query, so a re-minted link with a fresh token still finds its headers instead of playing bare. |
|
||||
| ITV full-list cache (`StalkerItvCacheService`) | catalog rows | Rows, not links. |
|
||||
| Downloads | the resolved `url` on the `downloads` row | **The one exception** — see below. |
|
||||
| Path | Stores | Verdict |
|
||||
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Recently Viewed | the raw row including `cmd` (`buildStalkerRecentlyViewedPayload` spreads the item) | Re-resolves on replay. |
|
||||
| Favorites | the raw row including `cmd` (`addStalkerFavorite`, `toggleFavorite`) | Re-resolves on replay. |
|
||||
| Playback positions | `playlist_id` + `content_xtream_id` + content type only — no URL column | Not applicable. |
|
||||
| Main-process playback context (`stalker-playback-context.service.ts`) | header sets keyed by the stream URL's origin + path, 15 min TTL | Stores no URL. The key drops the query, so a re-minted link with a fresh token still finds its headers instead of playing bare. |
|
||||
| ITV full-list cache (`StalkerItvCacheService`) | catalog rows | Rows, not links. |
|
||||
| Downloads | the resolved `url` on the `downloads` row | **The one exception** — see below. |
|
||||
|
||||
The download row is the only place a resolved URL outlives the playback that
|
||||
produced it, because the main-process downloader needs a URL it can retry and
|
||||
|
||||
Reference in new issue
Block a user