mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(stalker): tighten session protocol boundaries
This commit is contained in:
1 parent
f8a544d287
commit
92238ecbbb
13 files changed
+359
-78
No files matched your search
@@ -35,6 +35,7 @@ import { CatalogTitleMatch } from './catalog-title-match.interface';
|
||||
import { Settings } from './settings.interface';
|
||||
import {
|
||||
StalkerSessionConnectionOutcome,
|
||||
StalkerSessionApplicationOperation,
|
||||
StalkerSessionContinueRequest,
|
||||
StalkerSessionControlOutcome,
|
||||
StalkerSessionControlRequest,
|
||||
@@ -677,9 +678,11 @@ export interface ElectronBridgeApi {
|
||||
stalkerSessionContinue?: (
|
||||
request: StalkerSessionContinueRequest
|
||||
) => Promise<StalkerSessionConnectionOutcome>;
|
||||
stalkerSessionRequest?: (
|
||||
request: StalkerSessionRequest
|
||||
) => Promise<StalkerSessionRequestOutcome>;
|
||||
stalkerSessionRequest?: <
|
||||
Operation extends StalkerSessionApplicationOperation,
|
||||
>(
|
||||
request: StalkerSessionRequest<Operation>
|
||||
) => Promise<StalkerSessionRequestOutcome<Operation>>;
|
||||
stalkerSessionControl?: (
|
||||
request: StalkerSessionControlRequest
|
||||
) => Promise<StalkerSessionControlOutcome>;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type IpcCommandRequest,
|
||||
type IpcCommandResponse,
|
||||
} from './ipc-command.class';
|
||||
import type { ElectronBridgeApi } from './electron-api.interface';
|
||||
import {
|
||||
STALKER_SESSION_APPLICATION_OPERATIONS,
|
||||
type StalkerSessionApplicationOperation,
|
||||
@@ -70,6 +71,7 @@ describe('typed IPC commands', () => {
|
||||
authenticatedSession: true,
|
||||
playbackContext: true,
|
||||
},
|
||||
connectionMode: 'provisional',
|
||||
endpoint: 'https://portal.test/server/load.php',
|
||||
kind: 'ready',
|
||||
landingUrl: 'https://portal.test/c/',
|
||||
@@ -81,6 +83,7 @@ describe('typed IPC commands', () => {
|
||||
stalkerRecipeClassifierVersion: 1,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
},
|
||||
provisionalReason: 'import',
|
||||
recipe: 'full-session',
|
||||
requestId: 'request-id',
|
||||
};
|
||||
@@ -94,7 +97,8 @@ describe('typed IPC commands', () => {
|
||||
leaseRef: 'opaque-lease',
|
||||
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
|
||||
parameters: { category: '10', page: 1 },
|
||||
requestId: 'request-id',
|
||||
// @ts-expect-error Request IDs are generated by Electron main.
|
||||
requestId: 'renderer-request-id',
|
||||
} satisfies StalkerSessionRequest;
|
||||
const response: IpcCommandResponse<typeof STALKER_SESSION_REQUEST> = {
|
||||
kind: 'success',
|
||||
@@ -107,6 +111,43 @@ describe('typed IPC commands', () => {
|
||||
expect(response.kind).toBe('success');
|
||||
});
|
||||
|
||||
it('preserves operation correlation through IPC and Electron API aliases', () => {
|
||||
type CatalogOperation =
|
||||
typeof STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems;
|
||||
const request: IpcCommandRequest<
|
||||
typeof STALKER_SESSION_REQUEST,
|
||||
CatalogOperation
|
||||
> = {
|
||||
leaseRef: 'opaque-lease',
|
||||
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
|
||||
parameters: { page: 1 },
|
||||
};
|
||||
const response: IpcCommandResponse<
|
||||
typeof STALKER_SESSION_REQUEST,
|
||||
CatalogOperation
|
||||
> = {
|
||||
kind: 'success',
|
||||
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
|
||||
payload: { items: [] },
|
||||
requestId: 'main-request-id',
|
||||
};
|
||||
|
||||
const assertApi = (api: ElectronBridgeApi) => {
|
||||
const outcome = api.stalkerSessionRequest?.(request);
|
||||
outcome?.then((value) => {
|
||||
if (value.kind === 'success') {
|
||||
expect(value.payload.items).toEqual([]);
|
||||
// @ts-expect-error Catalog results never expose streamUrl.
|
||||
void value.payload.streamUrl;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
expect(request.operation).toBe('get-ordered-list');
|
||||
expect(response.kind).toBe('success');
|
||||
expect(assertApi).toBeDefined();
|
||||
});
|
||||
|
||||
it('correlates each operation with its request and sanitized result', () => {
|
||||
type MissingOperations = Exclude<
|
||||
StalkerSessionApplicationOperation,
|
||||
@@ -146,7 +187,8 @@ describe('typed IPC commands', () => {
|
||||
const request: IpcCommandRequest<typeof STALKER_SESSION_CONTROL> = {
|
||||
action: 'commit',
|
||||
attemptRef: 'opaque-attempt',
|
||||
requestId: 'request-id',
|
||||
// @ts-expect-error Request IDs are generated by Electron main.
|
||||
requestId: 'renderer-request-id',
|
||||
} satisfies StalkerSessionControlRequest;
|
||||
const response: IpcCommandResponse<typeof STALKER_SESSION_CONTROL> = {
|
||||
action: 'commit',
|
||||
@@ -158,6 +200,82 @@ describe('typed IPC commands', () => {
|
||||
expect(response.kind).toBe('success');
|
||||
});
|
||||
|
||||
it('discriminates persisted and provisional connection descriptors', () => {
|
||||
const persistedWithReason: StalkerSessionOpenRequest = {
|
||||
descriptor: {
|
||||
connectionMode: 'persisted-open',
|
||||
macAddress: '00:1A:79:00:00:01',
|
||||
playlistRef: 'playlist-ref',
|
||||
profilePreset: {
|
||||
id: 'mag250-public-5_1-minimal-v1',
|
||||
version: 1,
|
||||
},
|
||||
// @ts-expect-error Persisted opens never carry a provisional reason.
|
||||
provisionalReason: 'edit',
|
||||
sourceUrl: 'https://portal.test/c/',
|
||||
},
|
||||
};
|
||||
const provisionalWithoutReason: StalkerSessionOpenRequest = {
|
||||
// @ts-expect-error Provisional opens require their reason.
|
||||
descriptor: {
|
||||
connectionMode: 'provisional',
|
||||
macAddress: '00:1A:79:00:00:01',
|
||||
playlistRef: 'playlist-ref',
|
||||
profilePreset: {
|
||||
id: 'mag250-public-5_1-minimal-v1',
|
||||
version: 1,
|
||||
},
|
||||
sourceUrl: 'https://portal.test/c/',
|
||||
},
|
||||
};
|
||||
|
||||
expect(persistedWithReason.descriptor.connectionMode).toBe(
|
||||
'persisted-open'
|
||||
);
|
||||
expect(provisionalWithoutReason.descriptor.connectionMode).toBe(
|
||||
'provisional'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps playback context exclusive to a create-link result', () => {
|
||||
const catalogOutcome: StalkerSessionRequestOutcome = {
|
||||
kind: 'success',
|
||||
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
|
||||
payload: { items: [] },
|
||||
// @ts-expect-error Catalog success cannot carry playback context.
|
||||
playbackContextRef: 'opaque-context',
|
||||
requestId: 'main-request-id',
|
||||
};
|
||||
|
||||
expect(catalogOutcome.kind).toBe('success');
|
||||
});
|
||||
|
||||
it('excludes ready outcomes from application request results', () => {
|
||||
const requestOutcome: StalkerSessionRequestOutcome = {
|
||||
attemptRef: 'opaque-attempt',
|
||||
capabilities: {
|
||||
authenticatedSession: true,
|
||||
playbackContext: true,
|
||||
},
|
||||
endpoint: 'https://portal.test/server/load.php',
|
||||
// @ts-expect-error Ready belongs only to open/continue outcomes.
|
||||
kind: 'ready',
|
||||
landingUrl: 'https://portal.test/c/',
|
||||
leaseRef: 'opaque-lease',
|
||||
persistenceDraft: {
|
||||
isFullStalkerPortal: true,
|
||||
portalUrl: 'https://portal.test/server/load.php',
|
||||
stalkerLandingUrl: 'https://portal.test/c/',
|
||||
stalkerRecipeClassifierVersion: 1,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
},
|
||||
recipe: 'full-session',
|
||||
requestId: 'main-request-id',
|
||||
};
|
||||
|
||||
expect(requestOutcome.kind).toBe('ready');
|
||||
});
|
||||
|
||||
it('keeps secret runtime state out of result DTO examples', () => {
|
||||
const outcome: IpcCommandResponse<typeof STALKER_SESSION_OPEN> = {
|
||||
kind: 'failure',
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
StalkerSessionControlOutcome,
|
||||
StalkerSessionControlRequest,
|
||||
StalkerSessionOpenRequest,
|
||||
StalkerSessionApplicationOperation,
|
||||
StalkerSessionRequest,
|
||||
StalkerSessionRequestOutcome,
|
||||
} from './stalker-session.interface';
|
||||
@@ -34,16 +35,30 @@ export interface IpcCommandContractMap {
|
||||
}
|
||||
|
||||
export type IpcCommandId = keyof IpcCommandContractMap;
|
||||
export type IpcCommandRequest<Command extends IpcCommandId> =
|
||||
IpcCommandContractMap[Command]['request'];
|
||||
export type IpcCommandResponse<Command extends IpcCommandId> =
|
||||
IpcCommandContractMap[Command]['response'];
|
||||
export type IpcCommandRequest<
|
||||
Command extends IpcCommandId,
|
||||
Operation extends
|
||||
StalkerSessionApplicationOperation = StalkerSessionApplicationOperation,
|
||||
> = Command extends typeof STALKER_SESSION_REQUEST
|
||||
? StalkerSessionRequest<Operation>
|
||||
: IpcCommandContractMap[Command]['request'];
|
||||
export type IpcCommandResponse<
|
||||
Command extends IpcCommandId,
|
||||
Operation extends
|
||||
StalkerSessionApplicationOperation = StalkerSessionApplicationOperation,
|
||||
> = Command extends typeof STALKER_SESSION_REQUEST
|
||||
? StalkerSessionRequestOutcome<Operation>
|
||||
: IpcCommandContractMap[Command]['response'];
|
||||
|
||||
export class IpcCommand<Command extends IpcCommandId> {
|
||||
export class IpcCommand<
|
||||
Command extends IpcCommandId,
|
||||
Operation extends
|
||||
StalkerSessionApplicationOperation = StalkerSessionApplicationOperation,
|
||||
> {
|
||||
constructor(
|
||||
public readonly id: Command,
|
||||
public readonly callback: (
|
||||
payload: IpcCommandResponse<Command>
|
||||
payload: IpcCommandResponse<Command, Operation>
|
||||
) => void
|
||||
) {
|
||||
this.id = id;
|
||||
|
||||
@@ -27,6 +27,7 @@ export type StalkerSessionContentType =
|
||||
(typeof STALKER_SESSION_CONTENT_TYPES)[keyof typeof STALKER_SESSION_CONTENT_TYPES];
|
||||
|
||||
export type StalkerSessionEntityId = string | number;
|
||||
export type StalkerPlaybackContextRef = string;
|
||||
export type StalkerSessionNoParameters = Readonly<Record<string, never>>;
|
||||
|
||||
export interface StalkerSessionCategoryParameters {
|
||||
@@ -165,7 +166,7 @@ export interface StalkerSessionEpgResult {
|
||||
|
||||
export interface StalkerSessionCreateLinkResult {
|
||||
streamUrl: string;
|
||||
playbackContextRef?: string;
|
||||
playbackContextRef?: StalkerPlaybackContextRef;
|
||||
title?: string;
|
||||
thumbnail?: string | null;
|
||||
isLive?: boolean;
|
||||
|
||||
@@ -93,7 +93,6 @@ export type StalkerSessionStage =
|
||||
export type StalkerSessionLeaseRef = string;
|
||||
export type StalkerSessionAttemptRef = string;
|
||||
export type StalkerSessionChallengeRef = string;
|
||||
export type StalkerPlaybackContextRef = string;
|
||||
|
||||
export interface StalkerSessionProfilePreset {
|
||||
id: 'mag250-public-5_1-minimal-v1';
|
||||
@@ -128,10 +127,8 @@ export interface StalkerSessionTransportConfiguration {
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export interface StalkerSessionConnectionDescriptor {
|
||||
export interface StalkerSessionConnectionDescriptorBase {
|
||||
playlistRef: string;
|
||||
connectionMode: StalkerSessionConnectionMode;
|
||||
provisionalReason?: StalkerSessionProvisionalReason;
|
||||
sourceUrl: string;
|
||||
learnedEndpointHint?: string;
|
||||
macAddress: string;
|
||||
@@ -140,6 +137,22 @@ export interface StalkerSessionConnectionDescriptor {
|
||||
transportConfiguration?: StalkerSessionTransportConfiguration;
|
||||
}
|
||||
|
||||
export interface StalkerSessionPersistedConnectionDescriptor
|
||||
extends StalkerSessionConnectionDescriptorBase {
|
||||
connectionMode: 'persisted-open';
|
||||
provisionalReason?: never;
|
||||
}
|
||||
|
||||
export interface StalkerSessionProvisionalConnectionDescriptor
|
||||
extends StalkerSessionConnectionDescriptorBase {
|
||||
connectionMode: 'provisional';
|
||||
provisionalReason: StalkerSessionProvisionalReason;
|
||||
}
|
||||
|
||||
export type StalkerSessionConnectionDescriptor =
|
||||
| StalkerSessionPersistedConnectionDescriptor
|
||||
| StalkerSessionProvisionalConnectionDescriptor;
|
||||
|
||||
export interface StalkerSessionOpenRequest {
|
||||
descriptor: StalkerSessionConnectionDescriptor;
|
||||
}
|
||||
@@ -179,29 +192,47 @@ export interface StalkerSessionPersistenceDraft {
|
||||
isFullStalkerPortal: boolean;
|
||||
}
|
||||
|
||||
export interface StalkerSessionFullReadyOutcome {
|
||||
export interface StalkerSessionPersistedReadyState {
|
||||
connectionMode: 'persisted-open';
|
||||
provisionalReason?: never;
|
||||
attemptRef?: never;
|
||||
}
|
||||
|
||||
export interface StalkerSessionProvisionalReadyState {
|
||||
connectionMode: 'provisional';
|
||||
provisionalReason: StalkerSessionProvisionalReason;
|
||||
attemptRef: StalkerSessionAttemptRef;
|
||||
}
|
||||
|
||||
export interface StalkerSessionFullReadyOutcomeBase {
|
||||
kind: 'ready';
|
||||
recipe: 'full-session';
|
||||
requestId: string;
|
||||
endpoint: string;
|
||||
landingUrl: string;
|
||||
leaseRef: StalkerSessionLeaseRef;
|
||||
attemptRef?: StalkerSessionAttemptRef;
|
||||
capabilities: StalkerSessionCapabilities;
|
||||
accountSummary?: StalkerSessionAccountSummary;
|
||||
persistenceDraft: StalkerSessionPersistenceDraft;
|
||||
}
|
||||
|
||||
export interface StalkerSessionStatelessReadyOutcome {
|
||||
export type StalkerSessionFullReadyOutcome =
|
||||
StalkerSessionFullReadyOutcomeBase &
|
||||
(StalkerSessionPersistedReadyState | StalkerSessionProvisionalReadyState);
|
||||
|
||||
export interface StalkerSessionStatelessReadyOutcomeBase {
|
||||
kind: 'ready';
|
||||
recipe: 'stateless-mac';
|
||||
requestId: string;
|
||||
endpoint: string;
|
||||
landingUrl: string;
|
||||
attemptRef?: StalkerSessionAttemptRef;
|
||||
persistenceDraft: StalkerSessionPersistenceDraft;
|
||||
}
|
||||
|
||||
export type StalkerSessionStatelessReadyOutcome =
|
||||
StalkerSessionStatelessReadyOutcomeBase &
|
||||
(StalkerSessionPersistedReadyState | StalkerSessionProvisionalReadyState);
|
||||
|
||||
export interface StalkerSessionOriginApprovalRequiredOutcome {
|
||||
kind: 'origin-approval-required';
|
||||
requestId: string;
|
||||
@@ -234,39 +265,46 @@ export type StalkerSessionConnectionOutcome =
|
||||
| StalkerSessionCredentialsRequiredOutcome
|
||||
| StalkerSessionFailureOutcome;
|
||||
|
||||
export type StalkerSessionRequest = {
|
||||
[Operation in StalkerSessionApplicationOperation]: {
|
||||
export type StalkerSessionRequest<
|
||||
Operation extends
|
||||
StalkerSessionApplicationOperation = StalkerSessionApplicationOperation,
|
||||
> = {
|
||||
[CurrentOperation in Operation]: {
|
||||
leaseRef: StalkerSessionLeaseRef;
|
||||
operation: Operation;
|
||||
parameters: StalkerSessionOperationParameters<Operation>;
|
||||
requestId?: string;
|
||||
operation: CurrentOperation;
|
||||
parameters: StalkerSessionOperationParameters<CurrentOperation>;
|
||||
};
|
||||
}[StalkerSessionApplicationOperation];
|
||||
}[Operation];
|
||||
|
||||
export type StalkerSessionRequestSuccess = {
|
||||
[Operation in StalkerSessionApplicationOperation]: {
|
||||
export type StalkerSessionRequestSuccess<
|
||||
Operation extends
|
||||
StalkerSessionApplicationOperation = StalkerSessionApplicationOperation,
|
||||
> = {
|
||||
[CurrentOperation in Operation]: {
|
||||
kind: 'success';
|
||||
requestId: string;
|
||||
operation: Operation;
|
||||
payload: StalkerSessionOperationResult<Operation>;
|
||||
playbackContextRef?: StalkerPlaybackContextRef;
|
||||
operation: CurrentOperation;
|
||||
payload: StalkerSessionOperationResult<CurrentOperation>;
|
||||
};
|
||||
}[StalkerSessionApplicationOperation];
|
||||
}[Operation];
|
||||
|
||||
export type StalkerSessionRequestOutcome =
|
||||
| StalkerSessionRequestSuccess
|
||||
| StalkerSessionConnectionOutcome;
|
||||
export type StalkerSessionRequestOutcome<
|
||||
Operation extends
|
||||
StalkerSessionApplicationOperation = StalkerSessionApplicationOperation,
|
||||
> =
|
||||
| StalkerSessionRequestSuccess<Operation>
|
||||
| StalkerSessionOriginApprovalRequiredOutcome
|
||||
| StalkerSessionCredentialsRequiredOutcome
|
||||
| StalkerSessionFailureOutcome;
|
||||
|
||||
export interface StalkerSessionLeaseControlRequest {
|
||||
action: 'activate' | 'deactivate' | 'close' | 'force-redetect';
|
||||
leaseRef: StalkerSessionLeaseRef;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface StalkerSessionAttemptControlRequest {
|
||||
action: 'commit' | 'discard';
|
||||
attemptRef: StalkerSessionAttemptRef;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export type StalkerSessionControlRequest =
|
||||
|
||||
Reference in new issue
Block a user