fix: cookie picker auth token leak (v0.15.17.0) (#904)

* fix: cookie picker auth token leak (CVE — CVSS 7.8)

GET /cookie-picker served HTML that inlined the master bearer token
without authentication. Any local process could extract it and use it
to call /command, executing arbitrary JS in the browser context.

Fix: Jupyter-style one-time code exchange. The picker URL now includes
a one-time code that is consumed via 302 redirect, setting an HttpOnly
session cookie. The master AUTH_TOKEN never appears in HTML. The session
cookie is isolated from the scoped token system (not valid for /command).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump version and changelog (v0.15.17.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: browse-snapshot E2E turn budget too tight (7 → 9)

The agent consistently uses 8 turns for 5 snapshot commands because
it reads the saved annotated PNG to verify it was created. All 3 CI
attempts hit error_max_turns at exactly 8. Bumping to 9 gives headroom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-08 10:10:13 -07:00
committed by GitHub
parent b73f364411
commit a7593d70ef
8 changed files with 309 additions and 70 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## [0.16.1.0] - 2026-04-08
### Fixed
- Cookie picker no longer leaks the browse server auth token. Previously, opening the cookie picker page exposed the master bearer token in the HTML source, letting any local process extract it and execute arbitrary JavaScript in your browser session. Now uses a one-time code exchange with an HttpOnly session cookie. The token never appears in HTML, URLs, or browser history. (Reported by Horoshi at Vagabond Research, CVSS 7.8)
## [0.16.0.0] - 2026-04-07
### Added
+1 -1
View File
@@ -1 +1 @@
0.16.0.0
0.16.1.0
+92 -16
View File
@@ -4,20 +4,59 @@
* Handles all /cookie-picker/* routes. Imports from cookie-import-browser.ts
* (decryption) and cookie-picker-ui.ts (HTML generation).
*
* Routes (no auth — localhost-only, accepted risk):
* GET /cookie-picker → serves the picker HTML page
* GET /cookie-picker/browsers → list installed browsers
* GET /cookie-picker/domains → list domains + counts for a browser
* POST /cookie-picker/import → decrypt + import cookies to Playwright
* POST /cookie-picker/remove → clear cookies for domains
* GET /cookie-picker/imported → currently imported domains + counts
* Auth model (post-CVE fix):
* GET /cookie-picker → requires one-time code (?code=) or session cookie
* GET /cookie-picker/browsers → requires Bearer token or session cookie
* GET /cookie-picker/domains → requires Bearer token or session cookie
* POST /cookie-picker/import → requires Bearer token or session cookie
* POST /cookie-picker/remove → requires Bearer token or session cookie
* GET /cookie-picker/imported → requires Bearer token or session cookie
*
* The session cookie (gstack_picker) is isolated from the scoped token system.
* It is NOT valid for /command. This prevents session cookie extraction from
* re-enabling the auth token leak vulnerability.
*/
import * as crypto from 'crypto';
import type { BrowserManager } from './browser-manager';
import { findInstalledBrowsers, listProfiles, listDomains, importCookies, CookieImportError, type PlaywrightCookie } from './cookie-import-browser';
import { getCookiePickerHTML } from './cookie-picker-ui';
// ─── State ──────────────────────────────────────────────────────
// ─── Auth State ─────────────────────────────────────────────────
// One-time codes for the cookie picker UI (code → expiry timestamp).
// Codes are generated by generatePickerCode() and consumed on first use.
const pendingCodes = new Map<string, number>();
const CODE_TTL_MS = 30_000; // 30 seconds
// Session cookies for authenticated picker access (session → expiry timestamp).
// Sessions are created after a valid code exchange and last 1 hour.
const validSessions = new Map<string, number>();
const SESSION_TTL_MS = 3_600_000; // 1 hour
/** Generate a one-time code for opening the cookie picker UI. */
export function generatePickerCode(): string {
const code = crypto.randomUUID();
pendingCodes.set(code, Date.now() + CODE_TTL_MS);
return code;
}
/** Extract session ID from the gstack_picker cookie. */
function getSessionFromCookie(req: Request): string | null {
const cookie = req.headers.get('cookie');
if (!cookie) return null;
const match = cookie.match(/gstack_picker=([^;]+)/);
return match ? match[1] : null;
}
/** Check if a session cookie value is valid and not expired. */
function isValidSession(session: string): boolean {
const expiry = validSessions.get(session);
if (!expiry) return false;
if (Date.now() > expiry) { validSessions.delete(session); return false; }
return true;
}
// ─── Domain State ───────────────────────────────────────────────
// Tracks which domains were imported via the picker.
// /imported only returns cookies for domains in this Set.
// /remove clears from this Set.
@@ -71,19 +110,56 @@ export async function handleCookiePickerRoute(
}
try {
// GET /cookie-picker — serve the picker UI
// GET /cookie-picker — serve the picker UI (requires code or session cookie)
if (pathname === '/cookie-picker' && req.method === 'GET') {
const html = getCookiePickerHTML(port, authToken);
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
const code = url.searchParams.get('code');
// Code exchange: validate one-time code, set session cookie, redirect
if (code) {
const expiry = pendingCodes.get(code);
if (!expiry || Date.now() > expiry) {
pendingCodes.delete(code);
return new Response('Invalid or expired code. Re-run cookie-import-browser.', {
status: 403,
headers: { 'Content-Type': 'text/plain' },
});
}
pendingCodes.delete(code); // one-time use
const session = crypto.randomUUID();
validSessions.set(session, Date.now() + SESSION_TTL_MS);
return new Response(null, {
status: 302,
headers: {
'Location': '/cookie-picker',
'Set-Cookie': `gstack_picker=${session}; HttpOnly; SameSite=Strict; Path=/cookie-picker; Max-Age=3600`,
'Cache-Control': 'no-store',
},
});
}
// Session cookie: serve HTML (no auth token inlined)
const session = getSessionFromCookie(req);
if (session && isValidSession(session)) {
const html = getCookiePickerHTML(port);
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
// No code, no session: reject
return new Response('Access denied. Open the cookie picker from gstack.', {
status: 403,
headers: { 'Content-Type': 'text/plain' },
});
}
// ─── Auth gate: all data/action routes below require Bearer token ───
// Auth is mandatory — if authToken is undefined, reject all requests
// ─── Auth gate: all data/action routes below require Bearer token or session cookie ───
const authHeader = req.headers.get('authorization');
if (!authToken || !authHeader || authHeader !== `Bearer ${authToken}`) {
const sessionId = getSessionFromCookie(req);
const hasBearer = !!authToken && !!authHeader && authHeader === `Bearer ${authToken}`;
const hasSession = sessionId !== null && isValidSession(sessionId);
if (!hasBearer && !hasSession) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
+2 -5
View File
@@ -7,7 +7,7 @@
* No cookie values exposed anywhere.
*/
export function getCookiePickerHTML(serverPort: number, authToken?: string): string {
export function getCookiePickerHTML(serverPort: number): string {
const baseUrl = `http://127.0.0.1:${serverPort}`;
return `<!DOCTYPE html>
@@ -341,7 +341,6 @@ export function getCookiePickerHTML(serverPort: number, authToken?: string): str
<script>
(function() {
const BASE = '${baseUrl}';
const AUTH_TOKEN = '${authToken || ''}';
let activeBrowser = null;
let activeProfile = 'Default';
let allProfiles = [];
@@ -384,9 +383,7 @@ export function getCookiePickerHTML(serverPort: number, authToken?: string): str
// ─── API ────────────────────────────────
async function api(path, opts) {
const headers = { ...(opts?.headers || {}) };
if (AUTH_TOKEN) headers['Authorization'] = 'Bearer ' + AUTH_TOKEN;
const res = await fetch(BASE + '/cookie-picker' + path, { ...opts, headers });
const res = await fetch(BASE + '/cookie-picker' + path, { ...opts, credentials: 'same-origin' });
const data = await res.json();
if (!res.ok) {
const err = new Error(data.error || 'Request failed');
+4 -2
View File
@@ -8,6 +8,7 @@
import type { TabSession } from './tab-session';
import type { BrowserManager } from './browser-manager';
import { findInstalledBrowsers, importCookies, listSupportedBrowserNames } from './cookie-import-browser';
import { generatePickerCode } from './cookie-picker-routes';
import { validateNavigationUrl } from './url-validation';
import { validateOutputPath } from './path-security';
import * as fs from 'fs';
@@ -515,14 +516,15 @@ export async function handleWriteCommand(
throw new Error(`No Chromium browsers found. Supported: ${listSupportedBrowserNames().join(', ')}`);
}
const pickerUrl = `http://127.0.0.1:${port}/cookie-picker`;
const code = generatePickerCode();
const pickerUrl = `http://127.0.0.1:${port}/cookie-picker?code=${code}`;
try {
Bun.spawn(['open', pickerUrl], { stdout: 'ignore', stderr: 'ignore' });
} catch {
// open may fail silently — URL is in the message below
}
return `Cookie picker opened at ${pickerUrl}\nDetected browsers: ${browsers.map(b => b.name).join(', ')}\nSelect domains to import, then close the picker when done.`;
return `Cookie picker opened at http://127.0.0.1:${port}/cookie-picker\nDetected browsers: ${browsers.map(b => b.name).join(', ')}\nSelect domains to import, then close the picker when done.`;
}
case 'style': {
+180 -45
View File
@@ -2,11 +2,12 @@
* Tests for cookie-picker route handler
*
* Tests the HTTP glue layer directly with mock BrowserManager objects.
* Verifies that all routes return valid JSON (not HTML) with correct CORS headers.
* Verifies auth (one-time code exchange, session cookies, Bearer tokens),
* CORS headers, and JSON response formats.
*/
import { describe, test, expect } from 'bun:test';
import { handleCookiePickerRoute } from '../src/cookie-picker-routes';
import { handleCookiePickerRoute, generatePickerCode } from '../src/cookie-picker-routes';
// ─── Mock BrowserManager ──────────────────────────────────────
@@ -31,15 +32,28 @@ function makeUrl(path: string, port = 9470): URL {
return new URL(`http://127.0.0.1:${port}${path}`);
}
function makeReq(method: string, body?: any): Request {
const opts: RequestInit = { method };
function makeReq(method: string, body?: any, headers?: Record<string, string>): Request {
const opts: RequestInit = { method, headers: { ...headers } };
if (body) {
opts.body = JSON.stringify(body);
opts.headers = { 'Content-Type': 'application/json' };
(opts.headers as any)['Content-Type'] = 'application/json';
}
return new Request('http://127.0.0.1:9470', opts);
}
/** Helper: exchange a one-time code and return the session cookie value. */
async function getSessionCookie(bm: any, authToken: string): Promise<string> {
const code = generatePickerCode();
const url = makeUrl(`/cookie-picker?code=${code}`);
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res = await handleCookiePickerRoute(url, req, bm, authToken);
expect(res.status).toBe(302);
const setCookie = res.headers.get('Set-Cookie') || '';
const match = setCookie.match(/gstack_picker=([^;]+)/);
expect(match).not.toBeNull();
return match![1];
}
// ─── Tests ──────────────────────────────────────────────────────
describe('cookie-picker-routes', () => {
@@ -59,21 +73,27 @@ describe('cookie-picker-routes', () => {
test('JSON responses include correct CORS origin with port', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/browsers', 9450);
const req = new Request('http://127.0.0.1:9450', { method: 'GET' });
const req = new Request('http://127.0.0.1:9450', {
method: 'GET',
headers: { 'Authorization': 'Bearer test-token' },
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('http://127.0.0.1:9450');
});
});
describe('JSON responses (not HTML)', () => {
describe('JSON responses (with auth)', () => {
test('GET /cookie-picker/browsers returns JSON', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/browsers');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Authorization': 'Bearer test-token' },
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/json');
@@ -85,9 +105,12 @@ describe('cookie-picker-routes', () => {
test('GET /cookie-picker/domains without browser param returns JSON error', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/domains');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Authorization': 'Bearer test-token' },
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(400);
expect(res.headers.get('Content-Type')).toBe('application/json');
@@ -102,10 +125,13 @@ describe('cookie-picker-routes', () => {
const req = new Request('http://127.0.0.1:9470', {
method: 'POST',
body: 'not json',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token',
},
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(400);
expect(res.headers.get('Content-Type')).toBe('application/json');
@@ -116,9 +142,9 @@ describe('cookie-picker-routes', () => {
test('POST /cookie-picker/import missing browser field returns JSON error', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/import');
const req = makeReq('POST', { domains: ['.example.com'] });
const req = makeReq('POST', { domains: ['.example.com'] }, { 'Authorization': 'Bearer test-token' });
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(400);
const body = await res.json();
@@ -128,9 +154,9 @@ describe('cookie-picker-routes', () => {
test('POST /cookie-picker/import missing domains returns JSON error', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/import');
const req = makeReq('POST', { browser: 'Chrome' });
const req = makeReq('POST', { browser: 'Chrome' }, { 'Authorization': 'Bearer test-token' });
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(400);
const body = await res.json();
@@ -143,10 +169,13 @@ describe('cookie-picker-routes', () => {
const req = new Request('http://127.0.0.1:9470', {
method: 'POST',
body: '{bad',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token',
},
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(400);
expect(res.headers.get('Content-Type')).toBe('application/json');
@@ -155,9 +184,9 @@ describe('cookie-picker-routes', () => {
test('POST /cookie-picker/remove missing domains returns JSON error', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/remove');
const req = makeReq('POST', {});
const req = makeReq('POST', {}, { 'Authorization': 'Bearer test-token' });
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(400);
const body = await res.json();
@@ -167,9 +196,12 @@ describe('cookie-picker-routes', () => {
test('GET /cookie-picker/imported returns JSON with domain list', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/imported');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Authorization': 'Bearer test-token' },
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/json');
@@ -181,45 +213,148 @@ describe('cookie-picker-routes', () => {
});
describe('routing', () => {
test('GET /cookie-picker returns HTML', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res = await handleCookiePickerRoute(url, req, bm);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toContain('text/html');
});
test('unknown path returns 404', async () => {
test('unknown path returns 404 (with auth)', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/nonexistent');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Authorization': 'Bearer test-token' },
});
const res = await handleCookiePickerRoute(url, req, bm);
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(404);
});
});
describe('auth gate security', () => {
test('GET /cookie-picker HTML page works without auth token', async () => {
describe('one-time code exchange', () => {
test('valid code returns 302 redirect with session cookie', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker');
// Request with no Authorization header, but authToken is set on the server
const code = generatePickerCode();
const url = makeUrl(`/cookie-picker?code=${code}`);
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res = await handleCookiePickerRoute(url, req, bm, 'test-secret-token');
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(302);
expect(res.headers.get('Location')).toBe('/cookie-picker');
const setCookie = res.headers.get('Set-Cookie') || '';
expect(setCookie).toContain('gstack_picker=');
expect(setCookie).toContain('HttpOnly');
expect(setCookie).toContain('SameSite=Strict');
expect(setCookie).toContain('Path=/cookie-picker');
expect(setCookie).toContain('Max-Age=3600');
expect(res.headers.get('Cache-Control')).toBe('no-store');
});
test('code cannot be reused', async () => {
const { bm } = mockBrowserManager();
const code = generatePickerCode();
const url = makeUrl(`/cookie-picker?code=${code}`);
// First use: success
const req1 = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res1 = await handleCookiePickerRoute(url, req1, bm, 'test-token');
expect(res1.status).toBe(302);
// Second use: rejected
const req2 = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res2 = await handleCookiePickerRoute(url, req2, bm, 'test-token');
expect(res2.status).toBe(403);
});
test('invalid code returns 403', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker?code=not-a-valid-code');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(403);
});
test('GET /cookie-picker without code or session returns 403', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker');
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(403);
});
});
describe('session cookie auth', () => {
test('valid session cookie grants HTML access', async () => {
const { bm } = mockBrowserManager();
const session = await getSessionCookie(bm, 'test-token');
const url = makeUrl('/cookie-picker');
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Cookie': `gstack_picker=${session}` },
});
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toContain('text/html');
});
test('HTML response does NOT contain auth token', async () => {
const { bm } = mockBrowserManager();
const authToken = 'super-secret-auth-token-12345';
const session = await getSessionCookie(bm, authToken);
const url = makeUrl('/cookie-picker');
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Cookie': `gstack_picker=${session}` },
});
const res = await handleCookiePickerRoute(url, req, bm, authToken);
const html = await res.text();
expect(html).not.toContain(authToken);
expect(html).not.toContain('AUTH_TOKEN');
});
test('data routes accept session cookie', async () => {
const { bm } = mockBrowserManager();
const session = await getSessionCookie(bm, 'test-token');
const url = makeUrl('/cookie-picker/browsers');
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Cookie': `gstack_picker=${session}` },
});
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/json');
const body = await res.json();
expect(body).toHaveProperty('browsers');
});
test('invalid session cookie returns 403 for HTML', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker');
const req = new Request('http://127.0.0.1:9470', {
method: 'GET',
headers: { 'Cookie': 'gstack_picker=fake-session' },
});
const res = await handleCookiePickerRoute(url, req, bm, 'test-token');
expect(res.status).toBe(403);
});
});
describe('auth gate security', () => {
test('GET /cookie-picker/browsers returns 401 without auth', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/browsers');
// No Authorization header
const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
const res = await handleCookiePickerRoute(url, req, bm, 'test-secret-token');
@@ -241,7 +376,7 @@ describe('cookie-picker-routes', () => {
expect(body.error).toBe('Unauthorized');
});
test('GET /cookie-picker/browsers works with valid auth', async () => {
test('GET /cookie-picker/browsers works with valid Bearer auth', async () => {
const { bm } = mockBrowserManager();
const url = makeUrl('/cookie-picker/browsers');
const req = new Request('http://127.0.0.1:9470', {
+24
View File
@@ -317,4 +317,28 @@ describe('Server auth security', () => {
// The ownership check condition must exclude newtab
expect(ownershipBlock).toContain("command !== 'newtab'");
});
// CVE fix: cookie-picker HTML must NOT inline the auth token.
// getCookiePickerHTML() must not accept an authToken parameter.
test('cookie-picker UI does not accept or inline auth token', () => {
const uiSrc = fs.readFileSync(path.join(import.meta.dir, '../src/cookie-picker-ui.ts'), 'utf-8');
// Function signature must not include authToken
expect(uiSrc).not.toMatch(/getCookiePickerHTML\([^)]*authToken/);
// No AUTH_TOKEN interpolation in template
expect(uiSrc).not.toContain("AUTH_TOKEN = '${authToken");
expect(uiSrc).not.toContain("AUTH_TOKEN = '${auth");
});
// CVE fix: cookie-picker route handler uses one-time code exchange, not open access.
test('cookie-picker HTML route requires code or session cookie', () => {
const routeSrc = fs.readFileSync(path.join(import.meta.dir, '../src/cookie-picker-routes.ts'), 'utf-8');
// Must have code validation
expect(routeSrc).toContain('pendingCodes');
expect(routeSrc).toContain('validSessions');
// Must NOT pass authToken to getCookiePickerHTML
expect(routeSrc).not.toMatch(/getCookiePickerHTML\([^)]*authToken/);
// Must set HttpOnly session cookie
expect(routeSrc).toContain('HttpOnly');
expect(routeSrc).toContain('SameSite=Strict');
});
});
+1 -1
View File
@@ -68,7 +68,7 @@ Report the results of each command.`,
5. $B snapshot -i -a -o /tmp/skill-e2e-annotated.png
Report what each command returned.`,
workingDirectory: tmpDir,
maxTurns: 7,
maxTurns: 9,
timeout: 60_000,
testName: 'browse-snapshot',
runId,