Skip to content

Commit b1ad33b

Browse files
committed
fixup! feat(auth): params for embedding yir in apps
1 parent b1d2203 commit b1ad33b

23 files changed

Lines changed: 251 additions & 164 deletions

projects/client/src/hooks.client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import '$lib/polyfills/at.ts';
22
import '$lib/polyfills/mapGroupBy.ts';
33
import '$lib/polyfills/randomUUID.ts';
44
import '$lib/polyfills/toSorted.ts';
5+
import { captureWebviewSession } from '$lib/features/webview/captureWebviewSession.ts';
56
import { SENTRY_DSN } from '$lib/utils/constants.ts';
67
import { safeSessionStorage } from '$lib/utils/storage/safeStorage.ts';
78
import * as Sentry from '@sentry/sveltekit';
89
import { handleErrorWithSentry } from '@sentry/sveltekit';
910

11+
// Must run before Sentry.init and before SvelteKit reads `location`: strips the
12+
// WebView params (slurm VIP token, standalone flag) from the URL and latches
13+
// them to sessionStorage, so page.url, Sentry tracing/replay and analytics never
14+
// see the token.
15+
captureWebviewSession();
16+
1017
Sentry.init({
1118
dsn: SENTRY_DSN,
1219

projects/client/src/hooks.server.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { handle as handleTheme } from '$lib/features/theme/handle.ts';
1414
import { isBotAgent } from '$lib/utils/devices/isBotAgent.ts';
1515

1616
import { SENTRY_DSN } from '$lib/utils/constants.ts';
17+
import { stripWebviewParams } from '$lib/utils/url/stripWebviewParams.ts';
1718
import {
1819
handleErrorWithSentry,
1920
initCloudflareSentryHandle,
@@ -65,6 +66,26 @@ export const handleCacheControl: Handle = async ({ event, resolve }) => {
6566
});
6667
};
6768

69+
// Keep the slurm VIP token out of Sentry. The client strips the WebView params
70+
// before anything reads them, but the server request URL still carries them, so
71+
// scrub the request URL from every error and transaction report.
72+
function scrubWebviewParams<T extends { request?: { url?: string } }>(
73+
event: T,
74+
): T {
75+
const request = event.request;
76+
if (!request?.url) {
77+
return event;
78+
}
79+
80+
try {
81+
request.url = stripWebviewParams(new URL(request.url)).href;
82+
} catch {
83+
// Leave a non-absolute / unparseable URL untouched.
84+
}
85+
86+
return event;
87+
}
88+
6889
export const handle: Handle = sequence(
6990
initCloudflareSentryHandle({
7091
dsn: SENTRY_DSN,
@@ -76,6 +97,8 @@ export const handle: Handle = sequence(
7697
// whichever subrequest or body-read was in flight.
7798
'Network connection lost.',
7899
],
100+
beforeSend: scrubWebviewParams,
101+
beforeSendTransaction: scrubWebviewParams,
79102
}),
80103
sentryHandle(),
81104
// Must run before any feature that touches `event.locals` or session
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { safeSessionStorage } from '$lib/utils/storage/safeStorage.ts';
2+
3+
// The native apps append `standalone_mode` (see WEBVIEW_PARAMS) when they open a
4+
// YIR/MIR inside a WebView; when set, the app chrome is stripped so the page
5+
// reads as a native screen. The flag is captured at client boot
6+
// (captureWebviewSession) and latched here so it survives in-page navigation and
7+
// a param-less reload. WebView-local.
8+
const STORAGE_KEY = 'trakt-standalone-mode';
9+
10+
export function isStandaloneValue(value: string | null | undefined): boolean {
11+
return value === '1' || value === 'true';
12+
}
13+
14+
export function getStandalone(): boolean {
15+
return safeSessionStorage.getItem(STORAGE_KEY) === '1';
16+
}
17+
18+
export function setStandalone(): void {
19+
safeSessionStorage.setItem(STORAGE_KEY, '1');
20+
}

projects/client/src/lib/features/standalone/useStandaloneMode.svelte.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
isStandaloneValue,
3+
setStandalone,
4+
} from '$lib/features/standalone/standaloneFlag.ts';
5+
import { WEBVIEW_PARAMS } from '$lib/utils/url/webviewParams.ts';
6+
import { setSlurm } from './slurmToken.ts';
7+
8+
// Runs once at client boot (from hooks.client), before Sentry.init and before
9+
// SvelteKit reads `location` to build `page.url`. It latches the WebView params
10+
// into sessionStorage and strips them from the URL with a raw History replace.
11+
//
12+
// Doing it this early is the whole point: SvelteKit then builds `page.url` from
13+
// the already-clean `location`, so no downstream sink ever sees the slurm token
14+
// (Sentry tracing/replay, analytics page_view, shared links). A raw
15+
// replaceState is fine here precisely because it runs before `page.url` exists;
16+
// the same call from a component's onMount would not update `page.url`.
17+
export function captureWebviewSession(): void {
18+
if (typeof window === 'undefined') {
19+
return;
20+
}
21+
22+
const url = new URL(window.location.href);
23+
24+
const slurm = url.searchParams.get(WEBVIEW_PARAMS.slurm);
25+
if (slurm) {
26+
setSlurm(slurm);
27+
}
28+
29+
if (isStandaloneValue(url.searchParams.get(WEBVIEW_PARAMS.standaloneMode))) {
30+
setStandalone();
31+
}
32+
33+
const present = Object.values(WEBVIEW_PARAMS).filter((param) =>
34+
url.searchParams.has(param)
35+
);
36+
if (present.length === 0) {
37+
return;
38+
}
39+
40+
present.forEach((param) => url.searchParams.delete(param));
41+
window.history.replaceState(window.history.state, '', url);
42+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { page } from '$app/state';
2+
import { WEBVIEW_PARAMS } from '$lib/utils/url/webviewParams.ts';
3+
import { getSlurm } from './slurmToken.ts';
4+
5+
// Resolves the WebView VIP token for a YIR/MIR data request. On the client the
6+
// token was latched to sessionStorage at boot (captureWebviewSession) and the
7+
// URL is already clean, so getSlurm() wins; on the server (no storage) it falls
8+
// back to the URL param that is still present during SSR.
9+
export function resolveSlurm(): string | undefined {
10+
return getSlurm() ?? page.url.searchParams.get(WEBVIEW_PARAMS.slurm) ??
11+
undefined;
12+
}

projects/client/src/lib/features/webview/slurmToken.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { safeSessionStorage } from '$lib/utils/storage/safeStorage.ts';
22

33
// The native apps open a Year/Month in Review in a WebView with a `slurm` URL
4-
// param carrying the viewer's VIP token. It authorizes only the YIR/MIR data
5-
// requests (mirroring trakt.tv v2, where the backend resolves the token to the
6-
// viewing user)it is not an app-wide login session. Latched in
7-
// sessionStorage so it survives in-page navigation and a param-less reload,
4+
// param (see WEBVIEW_PARAMS) carrying the viewer's VIP token. It authorizes only
5+
// the YIR/MIR data requests (mirroring trakt.tv v2, where the backend resolves
6+
// the token to the viewing user); it is not an app-wide login session. Latched
7+
// in sessionStorage so it survives in-page navigation and a param-less reload,
88
// and stays WebView-local.
9-
export const SLURM_PARAM = 'slurm';
10-
119
const STORAGE_KEY = 'trakt-slurm';
1210

1311
export function getSlurm(): string | null {

projects/client/src/lib/features/webview/useSlurm.svelte.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { browser } from '$app/environment';
2+
import { page } from '$app/state';
3+
import {
4+
getStandalone,
5+
isStandaloneValue,
6+
} from '$lib/features/standalone/standaloneFlag.ts';
7+
import { WEBVIEW_PARAMS } from '$lib/utils/url/webviewParams.ts';
8+
import { getSlurm } from './slurmToken.ts';
9+
10+
// Exposes the WebView session params for a page. Capture + URL strip already
11+
// happened at client boot (captureWebviewSession, from hooks.client), so on the
12+
// client these read from sessionStorage while the URL is clean; on the server
13+
// (no storage) they read the URL params that are still present during SSR. Both
14+
// yield the same values, so the first client render matches SSR.
15+
export function useWebviewSession() {
16+
const slurm = browser
17+
? getSlurm() ?? undefined
18+
: page.url.searchParams.get(WEBVIEW_PARAMS.slurm) ?? undefined;
19+
20+
const isStandalone = browser ? getStandalone() : isStandaloneValue(
21+
page.url.searchParams.get(WEBVIEW_PARAMS.standaloneMode),
22+
);
23+
24+
return { slurm, isStandalone };
25+
}

projects/client/src/lib/requests/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ type RawApiFetchParams = {
88
fetch?: typeof fetch;
99
path: string;
1010
init?: Omit<RequestInit, 'headers'> & { headers?: Record<string, string> };
11+
// Defaults to true (attach the signed-in user's Bearer token). Pass false for
12+
// requests authorized out-of-band (e.g. the WebView `slurm` token): no Bearer
13+
// is sent, so a different account's token can never mix with the request, and
14+
// a 401 cannot tear down the web session.
15+
authenticated?: boolean;
1116
};
1217

1318
export type ApiParams = Omit<TraktApiOptions, 'apiKey' | 'environment'> & {
@@ -55,10 +60,13 @@ export const rawApiFetch = ({
5560
fetch = globalThis.fetch,
5661
path,
5762
init,
63+
authenticated = true,
5864
}: RawApiFetchParams) => {
5965
const { headers: additionalHeaders, ...restInit } = init ?? {};
60-
const authenticatedFetch = createAuthenticatedFetch(fetch);
61-
return authenticatedFetch(`${environment}${path}`, {
66+
// Unauthenticated requests use the bare fetch: no Bearer token, and none of
67+
// the 401 re-authentication side effects that would sign out a web session.
68+
const baseFetch = authenticated ? createAuthenticatedFetch(fetch) : fetch;
69+
return baseFetch(`${environment}${path}`, {
6270
...restInit,
6371
headers: {
6472
'trakt-api-version': '2',

0 commit comments

Comments
 (0)