Files
iptvnator/libs/services/src/lib/server-access-token.ts
T
4grayandClaude Opus 5.5 176108b682 fix(web): keep server storage off the startup bundle; form-field defaults on sign-in
PwaService imported isBackendUrl from the @iptvnator/services/server-storage
barrel, which pulled the whole storage subsystem (sync, migration, API
client) into main, though server mode loads it by dynamic import. The
helper now lives with the token helpers in @iptvnator/services
(re-exported from the interceptor): renderer.initialBytes 1,392,902 ->
1,357,795 bytes locally.

The remaining 11,988 bytes over #1931's baseline are what must run
before the first route (instance probe, connection status, token bound to
its instance, storage backend and write serialization); the baseline is
raised with a note, under the PR's perf-baseline-increase label.

The sign-in component provides the form-field defaults per component, as
master's check (#1928) now requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:58:25 +02:00

104 lines
3.1 KiB
TypeScript

import { SERVER_ACCESS_CHALLENGE } from '@iptvnator/shared/interfaces';
export const SERVER_ACCESS_TOKEN_STORAGE_KEY = 'iptvnator-server:access-token';
/**
* A gateway's answer for a backend that is down (nginx's 502/503/504 page),
* not the backend's own JSON error for a provider that failed.
*/
export function isGatewayOutage(
status: number,
contentType: string | null | undefined
): boolean {
return (
[502, 503, 504].includes(status) &&
!contentType?.includes('application/json')
);
}
/** A 401 from the backend's access guard, not one a provider sent. */
export function isServerAccessChallenge(
status: number,
challenge: string | null
): boolean {
return status === 401 && challenge === SERVER_ACCESS_CHALLENGE;
}
/** The instance the stored token was given for. */
export const SERVER_ACCESS_TOKEN_OWNER_KEY =
'iptvnator-server:access-token-instance';
/**
* The stored token; with `instanceId`, only when it was given for that
* instance: a tab still open on another server (a changed backend URL)
* never sends it there.
*/
export function readServerAccessToken(
instanceId?: string | null
): string | null {
try {
const token = localStorage
.getItem(SERVER_ACCESS_TOKEN_STORAGE_KEY)
?.trim();
return token &&
(instanceId === undefined ||
localStorage.getItem(SERVER_ACCESS_TOKEN_OWNER_KEY) ===
(instanceId ?? ''))
? token
: null;
} catch {
return null;
}
}
/** False when browser storage refused it (private mode, full). */
export function writeServerAccessToken(
token: string,
instanceId: string | null
): boolean {
try {
// The owner goes last: a token stored without one is sent nowhere.
localStorage.removeItem(SERVER_ACCESS_TOKEN_OWNER_KEY);
localStorage.setItem(SERVER_ACCESS_TOKEN_STORAGE_KEY, token.trim());
localStorage.setItem(SERVER_ACCESS_TOKEN_OWNER_KEY, instanceId ?? '');
return true;
} catch {
return false;
}
}
export function clearServerAccessToken(): void {
try {
localStorage.removeItem(SERVER_ACCESS_TOKEN_STORAGE_KEY);
localStorage.removeItem(SERVER_ACCESS_TOKEN_OWNER_KEY);
} catch {
// Nothing stored.
}
}
/**
* Whether `url` goes to the backend: the same origin as the backend URL and
* a path inside it. A plain prefix check fails for `BACKEND_URL=/`, where
* every URL, other origins included, would carry the token.
*/
export function isBackendUrl(
url: string,
baseUrl: string,
pageUrl = globalThis.location?.href ?? 'http://localhost/'
): boolean {
try {
const base = new URL(baseUrl, pageUrl);
const target = new URL(url, pageUrl);
const root = base.pathname.endsWith('/')
? base.pathname
: `${base.pathname}/`;
return (
target.origin === base.origin &&
(target.pathname === base.pathname ||
target.pathname.startsWith(root))
);
} catch {
return false;
}
}