diff --git a/apps/web/src/components/bug-report/bug-report-button.test.tsx b/apps/web/src/components/bug-report/bug-report-button.test.tsx index 487e12f..8e85627 100644 --- a/apps/web/src/components/bug-report/bug-report-button.test.tsx +++ b/apps/web/src/components/bug-report/bug-report-button.test.tsx @@ -8,14 +8,19 @@ import { computeCaptureSize } from '@/lib/bug-report-api'; /** * bug-report-button.test — NEU (quick-260914-m97, Fehler-melden-Knopf). * - * Elf Tests. Die Kernanforderung (Test 1): das Bild wird VOR dem Dialog - * aufgenommen — der `toPng`-Mock prueft waehrend seines Aufrufs, dass noch - * KEIN `role="dialog"` im DOM steht. `html-to-image` selbst laeuft in jsdom - * nicht (gemessen zur Planungszeit: `HTMLVideoElement is not defined`, kein - * Canvas-Backend) — deshalb der Mock; der Bildbeweis kommt aus dem Browser. + * Dreizehn Tests. Die Kernanforderung (Test 1): das Bild wird VOR dem + * Dialog aufgenommen — der `toPng`-Mock prueft waehrend seines Aufrufs, + * dass noch KEIN `role="dialog"` im DOM steht. `html-to-image` selbst + * laeuft in jsdom nicht (gemessen zur Planungszeit: + * `HTMLVideoElement is not defined`, kein Canvas-Backend) — deshalb der + * Mock; der Bildbeweis kommt aus dem Browser. * * Der next-intl-Mock liest die Texte aus der ECHTEN `de.json` (Muster * `tessera-logo.test.tsx`), Erwartungen zitieren `de.bugReport.`. + * + * Test 12/13 (quick-260918-gza): die vier Herkunftsfelder in der Nutzlast, + * abhaengig vom Cookie `tessera_desktop_client` — jsdom hat ohne gesetztes + * Cookie keinen Desktop-Client, darum bleibt Test 2 beim Browser-Fall. */ const { mockToPng, mockFetch, mockUser } = vi.hoisted(() => ({ mockToPng: vi.fn(), @@ -86,9 +91,15 @@ beforeEach(() => { vi.stubGlobal('fetch', mockFetch); }); +function clearDesktopCookies() { + document.cookie = 'tessera_desktop=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + document.cookie = 'tessera_desktop_client=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; +} + afterEach(() => { cleanup(); clearErrorBuffer(); + clearDesktopCookies(); vi.unstubAllGlobals(); }); @@ -142,6 +153,10 @@ describe('BugReportButton (quick-260914-m97)', () => { expect(body.get('userAgent')).toBeTruthy(); expect(body.get('viewport')).toMatch(/^\d+x\d+$/); expect(body.get('clientTime')).toMatch(/^\d{4}-\d{2}-\d{2}T/); + expect(body.get('clientKind')).toBe('browser'); + expect(body.get('clientOs')).toBe(''); + expect(body.get('clientVersion')).toBe(''); + expect(body.get('clientCommit')).toBe(''); expect(body.getAll('errors').some((e) => String(e).includes('GET /modules -> 500'))).toBe(true); const shot = body.get('screenshot'); expect(shot).toBeInstanceOf(Blob); @@ -271,6 +286,41 @@ describe('BugReportButton (quick-260914-m97)', () => { expect(screen.queryByText(specific)).toBeNull(); } }); + + it('Test 12 (quick-260918-gza): Desktop-Cookies gesetzt -> clientKind desktop mit Os/Version/Commit aus dem Cookie', async () => { + document.cookie = 'tessera_desktop=1; path=/'; + document.cookie = 'tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows; path=/'; + mockFetch.mockResolvedValue(jsonResponse(200, { sent: true })); + const user = userEvent.setup(); + await renderButton(); + await openDialog(user); + + await user.click(screen.getByRole('button', { name: T.send })); + await screen.findByText(T.sent); + + const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData; + expect(body.get('clientKind')).toBe('desktop'); + expect(body.get('clientOs')).toBe('windows'); + expect(body.get('clientVersion')).toBe('1.2.0'); + expect(body.get('clientCommit')).toBe('a6d1a64'); + }); + + it('Test 13 (quick-260918-gza, alter Client): nur tessera_desktop ohne tessera_desktop_client -> desktop ohne Details', async () => { + document.cookie = 'tessera_desktop=1; path=/'; + mockFetch.mockResolvedValue(jsonResponse(200, { sent: true })); + const user = userEvent.setup(); + await renderButton(); + await openDialog(user); + + await user.click(screen.getByRole('button', { name: T.send })); + await screen.findByText(T.sent); + + const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData; + expect(body.get('clientKind')).toBe('desktop'); + expect(body.get('clientOs')).toBe(''); + expect(body.get('clientVersion')).toBe(''); + expect(body.get('clientCommit')).toBe(''); + }); }); describe('computeCaptureSize (quick-260914-m97, reine Funktion)', () => { diff --git a/apps/web/src/components/bug-report/bug-report-dialog.tsx b/apps/web/src/components/bug-report/bug-report-dialog.tsx index 624bb40..0572920 100644 --- a/apps/web/src/components/bug-report/bug-report-dialog.tsx +++ b/apps/web/src/components/bug-report/bug-report-dialog.tsx @@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl'; import { useEffect, useRef, useState } from 'react'; import { appVersion } from '@/lib/app-version'; import { dataUrlToBlob, sendBugReport } from '@/lib/bug-report-api'; +import { getDesktopClientInfo, isDesktopClient } from '@/lib/desktop-client'; import { formatErrorsForReport } from '@/lib/error-buffer'; /** @@ -54,6 +55,11 @@ export function BugReportDialog({ open, screenshot, isAdmin, onClose }: BugRepor const handleSend = async () => { setStatus('sending'); + // Herkunft (quick-260918-gza): tessera_desktop entscheidet Desktop/ + // Browser, tessera_desktop_client liefert die Details dazu; fehlt es + // (alter Client), bleibt es bei Desktop ohne Details. + const desktop = isDesktopClient(); + const info = desktop ? getDesktopClientInfo() : null; const result = await sendBugReport({ description: description.trim(), page: window.location.pathname + window.location.search, @@ -65,6 +71,10 @@ export function BugReportDialog({ open, screenshot, isAdmin, onClose }: BugRepor clientTime: new Date().toISOString(), errors: formatErrorsForReport(), screenshot: attach && screenshot ? dataUrlToBlob(screenshot) : null, + clientKind: desktop ? 'desktop' : 'browser', + clientOs: info?.os ?? '', + clientVersion: info?.version ?? '', + clientCommit: info?.commit ?? '', }); if (result.ok) { setStatus('sent'); diff --git a/apps/web/src/lib/bug-report-api.test.ts b/apps/web/src/lib/bug-report-api.test.ts new file mode 100644 index 0000000..99eb4bb --- /dev/null +++ b/apps/web/src/lib/bug-report-api.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { sendBugReport } from './bug-report-api'; + +/** + * bug-report-api.test — NEU (quick-260918-gza). Bisher pruefte nur der + * Komponententest (`bug-report-button.test.tsx`) die FormData von + * `sendBugReport` — die reinen Nutzlastfelder (insbesondere die vier neuen + * Herkunftsfelder) gehoeren aber an die Funktion selbst, unabhaengig vom + * Dialog. Drei Tests: Desktop-Nutzlast, Browser-Nutzlast, Netzwerk-/ + * HTTP-Fehler. + */ +const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() })); + +const basePayload = { + description: 'x', + page: '/a', + webVersion: 'v1', + webChannel: 'beta', + webCommit: 'c', + userAgent: 'UA', + viewport: '1x1', + clientTime: 't', + errors: ['e1', 'e2'], + screenshot: null, +}; + +beforeEach(() => { + mockFetch.mockReset(); + mockFetch.mockResolvedValue(new Response('{}', { status: 200 })); + vi.stubGlobal('fetch', mockFetch); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('sendBugReport (quick-260918-gza)', () => { + it('Test 1: Desktop-Nutzlast — clientKind/clientOs/clientVersion/clientCommit im FormData, errors mehrfach, kein screenshot-Feld', async () => { + const result = await sendBugReport({ + ...basePayload, + clientKind: 'desktop', + clientOs: 'windows', + clientVersion: '1.2.0', + clientCommit: 'a6d1a64', + }); + + expect(result).toEqual({ ok: true }); + const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit]; + expect(String(url).endsWith('/bug-reports')).toBe(true); + expect(init.credentials).toBe('include'); + const body = init.body as FormData; + expect(body.get('clientKind')).toBe('desktop'); + expect(body.get('clientOs')).toBe('windows'); + expect(body.get('clientVersion')).toBe('1.2.0'); + expect(body.get('clientCommit')).toBe('a6d1a64'); + expect(body.getAll('errors')).toEqual(['e1', 'e2']); + expect(body.has('screenshot')).toBe(false); + }); + + it('Test 2: Browser-Nutzlast — die drei uebrigen Client-Felder sind vorhanden, aber leer (nicht weggelassen)', async () => { + await sendBugReport({ + ...basePayload, + clientKind: 'browser', + clientOs: '', + clientVersion: '', + clientCommit: '', + }); + + const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData; + expect(body.get('clientKind')).toBe('browser'); + expect(body.get('clientOs')).toBe(''); + expect(body.get('clientVersion')).toBe(''); + expect(body.get('clientCommit')).toBe(''); + }); + + it('Test 3: Netzwerkfehler -> { ok: false, status: 0 }; HTTP-Fehler -> { ok: false, status }', async () => { + mockFetch.mockRejectedValueOnce(new Error('offline')); + await expect( + sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }), + ).resolves.toEqual({ ok: false, status: 0 }); + + mockFetch.mockResolvedValueOnce(new Response('', { status: 429 })); + await expect( + sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }), + ).resolves.toEqual({ ok: false, status: 429 }); + }); +}); diff --git a/apps/web/src/lib/bug-report-api.ts b/apps/web/src/lib/bug-report-api.ts index 13d8949..27fd81e 100644 --- a/apps/web/src/lib/bug-report-api.ts +++ b/apps/web/src/lib/bug-report-api.ts @@ -13,6 +13,12 @@ * — das Bild als Datei, alle uebrigen Felder als Text — mit Cookie * (`credentials: 'include'`) und OHNE eigenen Content-Type-Header: die * Multipart-Grenze setzt der Browser selbst. + * + * Seit quick-260918-gza traegt die Nutzlast zusaetzlich vier Herkunftsfelder + * (`clientKind`, `clientOs`, `clientVersion`, `clientCommit`) — im Browser + * `clientKind=browser` mit leeren uebrigen Feldern, in der Desktop-App aus + * `getDesktopClientInfo()` (apps/web/src/lib/desktop-client.ts). Die API + * leitet daraus das Betreff-Kuerzel und die Zeile `Herkunft:` ab. */ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; @@ -79,6 +85,10 @@ export interface BugReportPayload { clientTime: string; errors: string[]; screenshot: Blob | null; + clientKind: 'desktop' | 'browser'; + clientOs: string; + clientVersion: string; + clientCommit: string; } export type BugReportResult = { ok: true } | { ok: false; status: number }; @@ -93,6 +103,10 @@ export async function sendBugReport(p: BugReportPayload): Promise { @@ -50,3 +60,50 @@ describe('useIsDesktopClient', () => { expect(result.current).toBe(false); }); }); + +describe('getDesktopClientInfo / parseDesktopClientCookie (quick-260918-gza)', () => { + it('kodierte Form (wie der Browser sie in document.cookie haelt)', () => { + expect( + parseDesktopClientCookie('tessera_desktop=1; tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows'), + ).toEqual({ version: '1.2.0', commit: 'a6d1a64', os: 'windows' }); + }); + + it('rohe Form (nicht kodiert)', () => { + expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0|a6d1a64|windows')).toEqual({ + version: '1.2.0', + commit: 'a6d1a64', + os: 'windows', + }); + }); + + it('leerer Commit bleibt leer', () => { + expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0%7C%7Clinux')).toEqual({ + version: '1.2.0', + commit: '', + os: 'linux', + }); + }); + + it('ohne Cookie: null', () => { + expect(parseDesktopClientCookie('tessera_desktop=1')).toBeNull(); + }); + + it('unvollstaendige oder leere Pflichtfelder: null', () => { + expect(parseDesktopClientCookie('tessera_desktop_client=abc')).toBeNull(); + expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0|x')).toBeNull(); + expect(parseDesktopClientCookie('tessera_desktop_client=|a6d1a64|linux')).toBeNull(); + expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0|a6d1a64|')).toBeNull(); + }); + + it('getDesktopClientInfo() liest document.cookie', () => { + expect(getDesktopClientInfo()).toBeNull(); + document.cookie = 'tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows; path=/'; + expect(getDesktopClientInfo()).toEqual({ version: '1.2.0', commit: 'a6d1a64', os: 'windows' }); + }); + + it('getDesktopClientInfo() ohne document: null', () => { + vi.stubGlobal('document', undefined); + expect(getDesktopClientInfo()).toBeNull(); + vi.unstubAllGlobals(); + }); +}); diff --git a/apps/web/src/lib/desktop-client.ts b/apps/web/src/lib/desktop-client.ts index 5860aac..c8f1d97 100644 --- a/apps/web/src/lib/desktop-client.ts +++ b/apps/web/src/lib/desktop-client.ts @@ -9,6 +9,58 @@ import { useEffect, useState } from 'react'; */ export const DESKTOP_COOKIE_NAME = 'tessera_desktop'; +/** + * Gegenstueck zu `buildDesktopClientCookieValue()` in der Middleware + * (quick-260918-gza): dort werden `dv`/`dc`/`dos` mit `${dv}|${dc}|${dos}` + * zusammengesetzt und von Next per `encodeURIComponent` serialisiert -- + * `|` wird `%7C` (gemessen). `document.cookie` liefert deshalb die + * KODIERTE Form; `parseDesktopClientCookie()` dekodiert (in try/catch, + * falls der Wert bereits roh vorliegt, z. B. direkt gesetzt im Test). + */ +export const DESKTOP_CLIENT_COOKIE_NAME = 'tessera_desktop_client'; + +export interface DesktopClientInfo { + version: string; + commit: string; + os: string; +} + +/** + * Parst den Wert des Cookies `tessera_desktop_client` aus einem rohen + * `document.cookie`-String. `null` bei fehlendem Cookie oder einer Form, + * die nicht genau drei durch `|` getrennte Teile mit nicht-leerer Version + * und nicht-leerem Betriebssystem ergibt (Commit darf leer sein). + */ +export function parseDesktopClientCookie(cookieString: string): DesktopClientInfo | null { + const entry = cookieString + .split(';') + .map((c) => c.trim()) + .find((c) => c.startsWith(`${DESKTOP_CLIENT_COOKIE_NAME}=`)); + if (!entry) return null; + + const raw = entry.slice(DESKTOP_CLIENT_COOKIE_NAME.length + 1); + let value = raw; + try { + value = decodeURIComponent(raw); + } catch { + // Rohwert (z. B. bereits dekodiert im Test) -- unveraendert weiterverwenden. + } + + const parts = value.split('|'); + if (parts.length !== 3) return null; + const [version, commit, os] = parts; + if (!version || !os) return null; + return { version, commit, os }; +} + +/** `null` ohne `document` (SSR) oder ohne gueltiges Cookie. */ +export function getDesktopClientInfo(): DesktopClientInfo | null { + if (typeof document === 'undefined') { + return null; + } + return parseDesktopClientCookie(document.cookie); +} + export function isDesktopClient(): boolean { if (typeof document === 'undefined') { return false; diff --git a/apps/web/src/middleware.test.ts b/apps/web/src/middleware.test.ts index 901e1b9..091f5d8 100644 --- a/apps/web/src/middleware.test.ts +++ b/apps/web/src/middleware.test.ts @@ -65,4 +65,52 @@ describe('middleware — Desktop-Client-Cookie (260917-h2s)', () => { expect(res.headers.get('set-cookie')).toContain('tessera_desktop=1'); expect(res.headers.get('x-middleware-next')).toBe('1'); }); + + it('Test 6 (quick-260918-gza): /login mit dv/dc/dos setzt zusaetzlich tessera_desktop_client', async () => { + const req = new NextRequest('http://localhost:3000/login?desktop=1&dv=1.2.0&dc=a6d1a64&dos=windows'); + const res = await middleware(req); + expect(res.cookies.get('tessera_desktop')?.value).toBe('1'); + expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|windows'); + + const setCookie = res.headers.get('set-cookie'); + expect(setCookie).toContain('tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows'); + expect(setCookie).toContain('Max-Age=31536000'); + expect(setCookie).toContain('Path=/'); + // Kein HttpOnly fuer DIESES Cookie -- der bestehende tessera_desktop + // liefert ebenfalls kein HttpOnly, darum genuegt die Wertpruefung oben. + }); + + it('Test 7 (quick-260918-gza, alter Client): /login?desktop=1 ohne dv/dc/dos setzt kein tessera_desktop_client', async () => { + const req = new NextRequest('http://localhost:3000/login?desktop=1'); + const res = await middleware(req); + expect(res.cookies.get('tessera_desktop')?.value).toBe('1'); + expect(res.cookies.get('tessera_desktop_client')).toBeUndefined(); + }); + + it('Test 8 (quick-260918-gza, Bereinigung): ungueltige oder fehlende Werte setzen kein tessera_desktop_client', async () => { + const angleBrackets = new NextRequest( + 'http://localhost:3000/login?desktop=1&dv=1.2.0%3Cscript%3E&dc=a6d1a64&dos=windows', + ); + expect((await middleware(angleBrackets)).cookies.get('tessera_desktop_client')).toBeUndefined(); + + const spaceInOs = new NextRequest( + 'http://localhost:3000/login?desktop=1&dv=1.2.0&dc=a6d1a64&dos=win%20dows', + ); + expect((await middleware(spaceInOs)).cookies.get('tessera_desktop_client')).toBeUndefined(); + + const missingDv = new NextRequest('http://localhost:3000/login?desktop=1&dc=a6d1a64&dos=windows'); + expect((await middleware(missingDv)).cookies.get('tessera_desktop_client')).toBeUndefined(); + + const emptyCommit = new NextRequest('http://localhost:3000/login?desktop=1&dv=1.2.0&dc=&dos=linux'); + expect((await middleware(emptyCommit)).cookies.get('tessera_desktop_client')?.value).toBe('1.2.0||linux'); + }); + + it('Test 9 (quick-260918-gza, Redirect-Pfad): /dashboard ohne Session setzt beide Cookies auf dem 307', async () => { + const req = new NextRequest('http://localhost:3000/dashboard?desktop=1&dv=1.2.0&dc=a6d1a64&dos=linux'); + const res = await middleware(req); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toContain('/login'); + expect(res.cookies.get('tessera_desktop')?.value).toBe('1'); + expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|linux'); + }); }); diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index dc6e7ad..798d78c 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -14,6 +14,46 @@ const publicRoutes = ['/login', '/reset-password']; const DESKTOP_COOKIE = 'tessera_desktop'; +/** + * Zweites Cookie (quick-260918-gza): traegt Version, Commit-Stempel und + * Betriebssystem der Desktop-App, aus denen der Fehler-melden-Knopf die + * Herkunft einer Meldung fuellt (Betreff-Kuerzel `[Desktop/Windows]` bzw. + * `[Desktop/Linux]` statt `[Browser]` -- WebView2 und WebKitGTK sehen im + * User-Agent sonst wie ein gewoehnlicher Browser aus). `httpOnly: false` + * ist Absicht: `getDesktopClientInfo()` (apps/web/src/lib/desktop-client.ts) + * liest es aus Seiten-JavaScript, Version/Betriebssystem sind kein Geheimnis + * -- dieselbe Vertrauensstufe wie der User-Agent, den jede Seite ohnehin + * liest. Next.js serialisiert den Wert mit `encodeURIComponent` + * (`|` wird `%7C`, gemessen zur Planungszeit). + */ +const DESKTOP_CLIENT_COOKIE = 'tessera_desktop_client'; + +/** Nur `[A-Za-z0-9.+_-]`, hoechstens 40 Zeichen, mindestens 1 Zeichen. */ +const DESKTOP_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,39}$/; +/** Leer erlaubt (kein Commit-Stempel im lokalen Bau), sonst hoechstens 40 Hex-/Alnum-Zeichen. */ +const DESKTOP_COMMIT_RE = /^[A-Za-z0-9]{0,40}$/; +/** `std::env::consts::OS`-Werte sind kleingeschrieben ASCII, hoechstens 20 Zeichen. */ +const DESKTOP_OS_RE = /^[a-z]{1,20}$/; + +/** + * Baut den Wert fuer `tessera_desktop_client` aus den Query-Parametern + * `dv`/`dc`/`dos` (T-GZA-03: feste Muster, Gesamtlaenge begrenzt). Fehlt + * einer der drei Parameter oder passt er nicht auf sein Muster, liefert + * die Funktion `null` -- die Middleware setzt das Cookie dann NICHT (ein + * alter Desktop-Client ohne diese Parameter bleibt ueber `tessera_desktop` + * als Desktop-App erkennbar, nur ohne Details). + */ +function buildDesktopClientCookieValue(params: URLSearchParams): string | null { + const dv = params.get('dv'); + const dc = params.get('dc'); + const dos = params.get('dos'); + if (dv === null || dc === null || dos === null) return null; + if (!DESKTOP_VERSION_RE.test(dv) || !DESKTOP_COMMIT_RE.test(dc) || !DESKTOP_OS_RE.test(dos)) { + return null; + } + return `${dv}|${dc}|${dos}`; +} + /** * Setzt das Cookie `tessera_desktop`, wenn die Anfrage `?desktop=1` traegt * (260917-h2s). Der Desktop-Client (apps/desktop/src-tauri/src/lib.rs, @@ -22,17 +62,25 @@ const DESKTOP_COOKIE = 'tessera_desktop'; * Fruehausstieg fuer oeffentliche Routen und auf Redirects -- sonst geht die * Kennung beim 307 nach /login verloren. `httpOnly: false` ist Absicht (wird * von `isDesktopClient()` in apps/web/src/lib/desktop-client.ts gelesen); - * der Wert ist kein Geheimnis. + * der Wert ist kein Geheimnis. Seit quick-260918-gza setzt dieselbe Funktion + * zusaetzlich das Cookie `tessera_desktop_client` (Version/Commit/OS), sofern + * die Anfrage gueltige Werte fuer `dv`/`dc`/`dos` mitbringt -- ohne sie wird + * das zweite Cookie weder gesetzt noch geloescht. */ function withDesktopCookie(req: NextRequest, res: NextResponse): NextResponse { if (req.nextUrl.searchParams.get('desktop') === '1') { - res.cookies.set(DESKTOP_COOKIE, '1', { + const cookieOptions = { path: '/', maxAge: 60 * 60 * 24 * 365, - sameSite: 'lax', + sameSite: 'lax' as const, httpOnly: false, secure: req.nextUrl.protocol === 'https:', - }); + }; + res.cookies.set(DESKTOP_COOKIE, '1', cookieOptions); + const info = buildDesktopClientCookieValue(req.nextUrl.searchParams); + if (info !== null) { + res.cookies.set(DESKTOP_CLIENT_COOKIE, info, cookieOptions); + } } return res; }