💬 Commit message: Update 2026-02-14 07:26:55, 21 files, 749 lines
📁 Files changed: 21 📝 Lines changed: 749 • browser.d.ts • browser.d.ts.map • browser.js • browser.js.map • mcp.js • mcp.js.map • safari.d.ts • safari.d.ts.map • safari.js • safari.js.map • safari.test.d.ts • safari.test.d.ts.map • safari.test.js • safari.test.js.map • types.d.ts • types.d.ts.map • browser.ts • mcp.ts • safari.test.ts • safari.ts • types.ts
This commit is contained in:
Vendored
+1
@@ -111,6 +111,7 @@ export declare class ClaudeBrowser {
|
|||||||
private handleDialogCommand;
|
private handleDialogCommand;
|
||||||
private handleCookiesCommand;
|
private handleCookiesCommand;
|
||||||
private handleStorageCommand;
|
private handleStorageCommand;
|
||||||
|
private handleImportCommand;
|
||||||
executeCommand(cmd: BrowserCommand): Promise<CommandResponse>;
|
executeCommand(cmd: BrowserCommand): Promise<CommandResponse>;
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=browser.d.ts.map
|
//# sourceMappingURL=browser.d.ts.map
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+34
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
|
|||||||
import { webkit } from 'playwright';
|
import { webkit } from 'playwright';
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
import * as image from './image.js';
|
import * as image from './image.js';
|
||||||
|
import * as safari from './safari.js';
|
||||||
export class ClaudeBrowser {
|
export class ClaudeBrowser {
|
||||||
browser = null;
|
browser = null;
|
||||||
context = null;
|
context = null;
|
||||||
@@ -669,6 +670,37 @@ export class ClaudeBrowser {
|
|||||||
return { ok: false, error: 'Unknown storage action' };
|
return { ok: false, error: 'Unknown storage action' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async handleImportCommand(cmd) {
|
||||||
|
const context = this.getContext();
|
||||||
|
if (!context)
|
||||||
|
throw new Error('Browser not launched');
|
||||||
|
if (cmd.source === 'safari') {
|
||||||
|
const cookies = await safari.importSafariCookies({
|
||||||
|
domain: cmd.domain,
|
||||||
|
profile: cmd.profile,
|
||||||
|
});
|
||||||
|
if (cookies.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
imported: 0,
|
||||||
|
source: 'safari',
|
||||||
|
domains: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Convert to Playwright format and add to context
|
||||||
|
const playwrightCookies = cookies.map(safari.toPlaywrightCookie);
|
||||||
|
await context.addCookies(playwrightCookies);
|
||||||
|
// Get unique domains for reporting
|
||||||
|
const domains = [...new Set(cookies.map((c) => c.domain))];
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
imported: cookies.length,
|
||||||
|
source: 'safari',
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: false, error: `Unknown import source: ${cmd.source}` };
|
||||||
|
}
|
||||||
async executeCommand(cmd) {
|
async executeCommand(cmd) {
|
||||||
try {
|
try {
|
||||||
switch (cmd.cmd) {
|
switch (cmd.cmd) {
|
||||||
@@ -856,6 +888,8 @@ export class ClaudeBrowser {
|
|||||||
size: result.size,
|
size: result.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'import':
|
||||||
|
return this.handleImportCommand(cmd);
|
||||||
default: {
|
default: {
|
||||||
const _exhaustive = cmd;
|
const _exhaustive = cmd;
|
||||||
return { ok: false, error: `Unknown command: ${_exhaustive.cmd}` };
|
return { ok: false, error: `Unknown command: ${_exhaustive.cmd}` };
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+14
-5
@@ -69,11 +69,7 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
|
|||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(false)
|
||||||
.describe('Highlight elements before actions with visual overlay'),
|
.describe('Highlight elements before actions with visual overlay'),
|
||||||
previewDelay: z
|
previewDelay: z.number().optional().default(2000).describe('Preview highlight duration in ms'),
|
||||||
.number()
|
|
||||||
.optional()
|
|
||||||
.default(2000)
|
|
||||||
.describe('Preview highlight duration in ms'),
|
|
||||||
width: z.number().optional().default(1280).describe('Viewport width'),
|
width: z.number().optional().default(1280).describe('Viewport width'),
|
||||||
height: z.number().optional().default(800).describe('Viewport height'),
|
height: z.number().optional().default(800).describe('Viewport height'),
|
||||||
}, withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height }) => {
|
}, withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height }) => {
|
||||||
@@ -450,6 +446,19 @@ server.tool('session_restore', 'Restore a previously saved session state from a
|
|||||||
savedAt: data.savedAt,
|
savedAt: data.savedAt,
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
// Browser import
|
||||||
|
server.tool('import', 'Import cookies from Safari browser (macOS only). Requires Full Disk Access permission.', {
|
||||||
|
source: z.enum(['safari']).describe('Browser to import from (currently only Safari supported)'),
|
||||||
|
domain: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Filter cookies to specific domain (e.g., "github.com")'),
|
||||||
|
profile: z.string().optional().describe('Safari profile/WebKit data store ID (optional)'),
|
||||||
|
}, withLogging('import', async ({ source, domain, profile }) => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.executeCommand({ cmd: 'import', source, domain, profile });
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
}));
|
||||||
// Image processing
|
// Image processing
|
||||||
server.tool('favicon', 'Generate a complete favicon set from an image (16x16, 32x32, 48x48, apple-touch-icon 180x180, android-chrome 192x192 and 512x512)', {
|
server.tool('favicon', 'Generate a complete favicon set from an image (16x16, 32x32, 48x48, apple-touch-icon 180x180, android-chrome 192x192 and 512x512)', {
|
||||||
input: z.string().describe('Path to source image'),
|
input: z.string().describe('Path to source image'),
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Safari Cookies.binarycookies parser
|
||||||
|
*
|
||||||
|
* Format specification: https://github.com/libyal/dtformats/blob/main/documentation/Safari%20Cookies.asciidoc
|
||||||
|
*
|
||||||
|
* File structure:
|
||||||
|
* - Header: "cook" magic + page count + page sizes array
|
||||||
|
* - Pages: Each contains cookie records
|
||||||
|
* - Footer: 8 bytes (checksum)
|
||||||
|
*/
|
||||||
|
export interface SafariCookie {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
expires: number;
|
||||||
|
secure: boolean;
|
||||||
|
httpOnly: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a Safari Cookies.binarycookies file
|
||||||
|
*/
|
||||||
|
export declare function parseBinaryCookies(filePath: string): Promise<SafariCookie[]>;
|
||||||
|
/**
|
||||||
|
* Get the default Safari cookies file path
|
||||||
|
*/
|
||||||
|
export declare function getSafariCookiesPath(profile?: string): string;
|
||||||
|
/**
|
||||||
|
* List available Safari profiles (WebKit data stores)
|
||||||
|
*/
|
||||||
|
export declare function listSafariProfiles(): Promise<string[]>;
|
||||||
|
/**
|
||||||
|
* Import Safari cookies, optionally filtered by domain
|
||||||
|
*/
|
||||||
|
export declare function importSafariCookies(options?: {
|
||||||
|
profile?: string;
|
||||||
|
domain?: string;
|
||||||
|
}): Promise<SafariCookie[]>;
|
||||||
|
/**
|
||||||
|
* Convert SafariCookie to Playwright cookie format
|
||||||
|
*/
|
||||||
|
export declare function toPlaywrightCookie(cookie: SafariCookie): {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
expires: number;
|
||||||
|
secure: boolean;
|
||||||
|
httpOnly: boolean;
|
||||||
|
sameSite: 'Strict' | 'Lax' | 'None';
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=safari.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"safari.d.ts","sourceRoot":"","sources":["../src/safari.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AA2FD;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAkClF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB7D;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAqB5D;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0B1B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CACrC,CAWA"}
|
||||||
Vendored
+195
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Safari Cookies.binarycookies parser
|
||||||
|
*
|
||||||
|
* Format specification: https://github.com/libyal/dtformats/blob/main/documentation/Safari%20Cookies.asciidoc
|
||||||
|
*
|
||||||
|
* File structure:
|
||||||
|
* - Header: "cook" magic + page count + page sizes array
|
||||||
|
* - Pages: Each contains cookie records
|
||||||
|
* - Footer: 8 bytes (checksum)
|
||||||
|
*/
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
// Cocoa epoch (Jan 1, 2001) to Unix epoch (Jan 1, 1970) offset in seconds
|
||||||
|
const COCOA_EPOCH_OFFSET = 978307200;
|
||||||
|
/**
|
||||||
|
* Convert Cocoa timestamp (seconds since Jan 1, 2001) to Unix timestamp
|
||||||
|
*/
|
||||||
|
function cocoaToUnix(cocoaTime) {
|
||||||
|
return Math.floor(cocoaTime + COCOA_EPOCH_OFFSET);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read a null-terminated string from buffer at offset
|
||||||
|
*/
|
||||||
|
function readCString(buffer, offset) {
|
||||||
|
let end = offset;
|
||||||
|
while (end < buffer.length && buffer[end] !== 0) {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
return buffer.subarray(offset, end).toString('utf8');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a single cookie record from a page
|
||||||
|
*/
|
||||||
|
function parseCookieRecord(buffer, recordOffset) {
|
||||||
|
// Cookie record structure (little-endian):
|
||||||
|
// 0-3: Record size
|
||||||
|
// 4-7: Unknown
|
||||||
|
// 8-11: Flags (1=secure, 4=httpOnly)
|
||||||
|
// 12-15: Unknown
|
||||||
|
// 16-19: URL/domain offset (relative to record start)
|
||||||
|
// 20-23: Name offset
|
||||||
|
// 24-27: Path offset
|
||||||
|
// 28-31: Value offset
|
||||||
|
// 32-39: Unknown (end marker)
|
||||||
|
// 40-47: Expiration (64-bit float, Cocoa timestamp)
|
||||||
|
// 48-55: Creation (64-bit float, Cocoa timestamp)
|
||||||
|
// 56+: String data
|
||||||
|
const flags = buffer.readUInt32LE(recordOffset + 8);
|
||||||
|
const domainOffset = buffer.readUInt32LE(recordOffset + 16);
|
||||||
|
const nameOffset = buffer.readUInt32LE(recordOffset + 20);
|
||||||
|
const pathOffset = buffer.readUInt32LE(recordOffset + 24);
|
||||||
|
const valueOffset = buffer.readUInt32LE(recordOffset + 28);
|
||||||
|
const expiration = buffer.readDoubleLE(recordOffset + 40);
|
||||||
|
return {
|
||||||
|
name: readCString(buffer, recordOffset + nameOffset),
|
||||||
|
value: readCString(buffer, recordOffset + valueOffset),
|
||||||
|
domain: readCString(buffer, recordOffset + domainOffset),
|
||||||
|
path: readCString(buffer, recordOffset + pathOffset),
|
||||||
|
expires: cocoaToUnix(expiration),
|
||||||
|
secure: (flags & 0x01) !== 0,
|
||||||
|
httpOnly: (flags & 0x04) !== 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a page of cookies
|
||||||
|
*/
|
||||||
|
function parsePage(buffer) {
|
||||||
|
const cookies = [];
|
||||||
|
// Page header:
|
||||||
|
// 0-3: Page signature (0x00000100 as big-endian)
|
||||||
|
// 4-7: Number of cookies (little-endian)
|
||||||
|
// 8+: Cookie record offsets array (little-endian)
|
||||||
|
const signature = buffer.readUInt32BE(0);
|
||||||
|
if (signature !== 0x00000100) {
|
||||||
|
// Invalid page signature, skip
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
const cookieCount = buffer.readUInt32LE(4);
|
||||||
|
for (let i = 0; i < cookieCount; i++) {
|
||||||
|
const recordOffset = buffer.readUInt32LE(8 + i * 4);
|
||||||
|
try {
|
||||||
|
const cookie = parseCookieRecord(buffer, recordOffset);
|
||||||
|
cookies.push(cookie);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Skip malformed cookie records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a Safari Cookies.binarycookies file
|
||||||
|
*/
|
||||||
|
export async function parseBinaryCookies(filePath) {
|
||||||
|
const buffer = await readFile(filePath);
|
||||||
|
const cookies = [];
|
||||||
|
// File header (big-endian):
|
||||||
|
// 0-3: Magic "cook"
|
||||||
|
// 4-7: Number of pages
|
||||||
|
// 8+: Page sizes array (4 bytes each)
|
||||||
|
const magic = buffer.subarray(0, 4).toString('ascii');
|
||||||
|
if (magic !== 'cook') {
|
||||||
|
throw new Error(`Invalid binarycookies file: expected "cook" magic, got "${magic}"`);
|
||||||
|
}
|
||||||
|
const pageCount = buffer.readUInt32BE(4);
|
||||||
|
const pageSizes = [];
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
pageSizes.push(buffer.readUInt32BE(8 + i * 4));
|
||||||
|
}
|
||||||
|
// Calculate where pages start (after header)
|
||||||
|
let pageOffset = 8 + pageCount * 4;
|
||||||
|
// Parse each page
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
const pageSize = pageSizes[i];
|
||||||
|
const pageBuffer = buffer.subarray(pageOffset, pageOffset + pageSize);
|
||||||
|
const pageCookies = parsePage(pageBuffer);
|
||||||
|
cookies.push(...pageCookies);
|
||||||
|
pageOffset += pageSize;
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the default Safari cookies file path
|
||||||
|
*/
|
||||||
|
export function getSafariCookiesPath(profile) {
|
||||||
|
const home = homedir();
|
||||||
|
if (profile) {
|
||||||
|
// Profile-specific WebKit data store
|
||||||
|
return join(home, 'Library/Containers/com.apple.Safari/Data/Library/WebKit/WebsiteDataStore', profile, 'Cookies/Cookies.binarycookies');
|
||||||
|
}
|
||||||
|
// Default Safari cookies location
|
||||||
|
return join(home, 'Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* List available Safari profiles (WebKit data stores)
|
||||||
|
*/
|
||||||
|
export async function listSafariProfiles() {
|
||||||
|
const { readdir } = await import('node:fs/promises');
|
||||||
|
const home = homedir();
|
||||||
|
const webkitPath = join(home, 'Library/Containers/com.apple.Safari/Data/Library/WebKit/WebsiteDataStore');
|
||||||
|
try {
|
||||||
|
const entries = await readdir(webkitPath, { withFileTypes: true });
|
||||||
|
return entries
|
||||||
|
.filter((e) => e.isDirectory())
|
||||||
|
.map((e) => e.name)
|
||||||
|
.filter((name) => {
|
||||||
|
// Check if this profile has a cookies file
|
||||||
|
const cookiePath = join(webkitPath, name, 'Cookies/Cookies.binarycookies');
|
||||||
|
return existsSync(cookiePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Import Safari cookies, optionally filtered by domain
|
||||||
|
*/
|
||||||
|
export async function importSafariCookies(options) {
|
||||||
|
const cookiesPath = getSafariCookiesPath(options?.profile);
|
||||||
|
if (!existsSync(cookiesPath)) {
|
||||||
|
throw new Error(`Safari cookies file not found at: ${cookiesPath}\nMake sure Safari has been used and Full Disk Access is granted to your terminal.`);
|
||||||
|
}
|
||||||
|
let cookies = await parseBinaryCookies(cookiesPath);
|
||||||
|
// Filter by domain if specified
|
||||||
|
if (options?.domain) {
|
||||||
|
const domainFilter = options.domain.toLowerCase();
|
||||||
|
cookies = cookies.filter((c) => {
|
||||||
|
const cookieDomain = c.domain.toLowerCase();
|
||||||
|
// Match exact domain or subdomain (e.g., ".example.com" matches "sub.example.com")
|
||||||
|
return (cookieDomain === domainFilter ||
|
||||||
|
cookieDomain === `.${domainFilter}` ||
|
||||||
|
cookieDomain.endsWith(`.${domainFilter}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert SafariCookie to Playwright cookie format
|
||||||
|
*/
|
||||||
|
export function toPlaywrightCookie(cookie) {
|
||||||
|
return {
|
||||||
|
name: cookie.name,
|
||||||
|
value: cookie.value,
|
||||||
|
domain: cookie.domain,
|
||||||
|
path: cookie.path,
|
||||||
|
expires: cookie.expires,
|
||||||
|
secure: cookie.secure,
|
||||||
|
httpOnly: cookie.httpOnly,
|
||||||
|
sameSite: cookie.secure ? 'None' : 'Lax', // Best guess since Safari doesn't store this
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=safari.js.map
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=safari.test.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"safari.test.d.ts","sourceRoot":"","sources":["../src/safari.test.ts"],"names":[],"mappings":""}
|
||||||
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { toPlaywrightCookie } from './safari.js';
|
||||||
|
describe('safari', () => {
|
||||||
|
describe('toPlaywrightCookie', () => {
|
||||||
|
it('should convert SafariCookie to Playwright format', () => {
|
||||||
|
const safariCookie = {
|
||||||
|
name: 'session_id',
|
||||||
|
value: 'abc123',
|
||||||
|
domain: '.example.com',
|
||||||
|
path: '/',
|
||||||
|
expires: 1735689600, // 2025-01-01
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true,
|
||||||
|
};
|
||||||
|
const result = toPlaywrightCookie(safariCookie);
|
||||||
|
expect(result.name).toBe('session_id');
|
||||||
|
expect(result.value).toBe('abc123');
|
||||||
|
expect(result.domain).toBe('.example.com');
|
||||||
|
expect(result.path).toBe('/');
|
||||||
|
expect(result.expires).toBe(1735689600);
|
||||||
|
expect(result.secure).toBe(true);
|
||||||
|
expect(result.httpOnly).toBe(true);
|
||||||
|
expect(result.sameSite).toBe('None'); // Secure cookies get SameSite=None
|
||||||
|
});
|
||||||
|
it('should set SameSite to Lax for non-secure cookies', () => {
|
||||||
|
const safariCookie = {
|
||||||
|
name: 'tracking',
|
||||||
|
value: 'xyz',
|
||||||
|
domain: 'example.com',
|
||||||
|
path: '/',
|
||||||
|
expires: 1735689600,
|
||||||
|
secure: false,
|
||||||
|
httpOnly: false,
|
||||||
|
};
|
||||||
|
const result = toPlaywrightCookie(safariCookie);
|
||||||
|
expect(result.secure).toBe(false);
|
||||||
|
expect(result.sameSite).toBe('Lax');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=safari.test.js.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"safari.test.js","sourceRoot":"","sources":["../src/safari.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAqB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEpE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;IACtB,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAClC,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;YAC1D,MAAM,YAAY,GAAiB;gBACjC,IAAI,EAAE,YAAY;gBAClB,KAAK,EAAE,QAAQ;gBACf,MAAM,EAAE,cAAc;gBACtB,IAAI,EAAE,GAAG;gBACT,OAAO,EAAE,UAAU,EAAE,aAAa;gBAClC,MAAM,EAAE,IAAI;gBACZ,QAAQ,EAAE,IAAI;aACf,CAAC;YAEF,MAAM,MAAM,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;YAEhD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACvC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC9B,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACxC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,mCAAmC;QAC3E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;YAC3D,MAAM,YAAY,GAAiB;gBACjC,IAAI,EAAE,UAAU;gBAChB,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE,aAAa;gBACrB,IAAI,EAAE,GAAG;gBACT,OAAO,EAAE,UAAU;gBACnB,MAAM,EAAE,KAAK;gBACb,QAAQ,EAAE,KAAK;aAChB,CAAC;YAEF,MAAM,MAAM,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;YAEhD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||||
Vendored
+10
-1
@@ -252,7 +252,13 @@ export interface EmulateCommand {
|
|||||||
cmd: 'emulate';
|
cmd: 'emulate';
|
||||||
device: string;
|
device: string;
|
||||||
}
|
}
|
||||||
export type BrowserCommand = GotoCommand | ClickCommand | TypeCommand | QueryCommand | ScreenshotCommand | UrlCommand | HtmlCommand | BackCommand | ForwardCommand | ReloadCommand | WaitCommand | NewPageCommand | CloseCommand | EvalCommand | FaviconCommand | ConvertCommand | ResizeCommand | CropCommand | CompressCommand | ThumbnailCommand | ConsoleCommand | NetworkCommand | InterceptCommand | ErrorsCommand | MetricsCommand | A11yCommand | DialogCommand | CookiesCommand | StorageCommand | HoverCommand | SelectCommand | KeysCommand | UploadCommand | ScrollCommand | ViewportCommand | EmulateCommand;
|
export interface ImportCommand {
|
||||||
|
cmd: 'import';
|
||||||
|
source: 'safari';
|
||||||
|
domain?: string;
|
||||||
|
profile?: string;
|
||||||
|
}
|
||||||
|
export type BrowserCommand = GotoCommand | ClickCommand | TypeCommand | QueryCommand | ScreenshotCommand | UrlCommand | HtmlCommand | BackCommand | ForwardCommand | ReloadCommand | WaitCommand | NewPageCommand | CloseCommand | EvalCommand | FaviconCommand | ConvertCommand | ResizeCommand | CropCommand | CompressCommand | ThumbnailCommand | ConsoleCommand | NetworkCommand | InterceptCommand | ErrorsCommand | MetricsCommand | A11yCommand | DialogCommand | CookiesCommand | StorageCommand | HoverCommand | SelectCommand | KeysCommand | UploadCommand | ScrollCommand | ViewportCommand | EmulateCommand | ImportCommand;
|
||||||
export interface SuccessResponse {
|
export interface SuccessResponse {
|
||||||
ok: true;
|
ok: true;
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -291,6 +297,9 @@ export interface SuccessResponse {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
selected?: string[];
|
selected?: string[];
|
||||||
|
imported?: number;
|
||||||
|
source?: string;
|
||||||
|
domains?: string[];
|
||||||
}
|
}
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
ok: false;
|
ok: false;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import { type Browser, type BrowserContext, type Page, type Route, webkit } from
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
import * as image from './image.js';
|
import * as image from './image.js';
|
||||||
|
import * as safari from './safari.js';
|
||||||
import type {
|
import type {
|
||||||
A11yNode,
|
A11yNode,
|
||||||
BrowserCommand,
|
BrowserCommand,
|
||||||
@@ -15,6 +16,7 @@ import type {
|
|||||||
DialogCommand,
|
DialogCommand,
|
||||||
DialogEntry,
|
DialogEntry,
|
||||||
ElementInfo,
|
ElementInfo,
|
||||||
|
ImportCommand,
|
||||||
MetricsData,
|
MetricsData,
|
||||||
NetworkEntry,
|
NetworkEntry,
|
||||||
PageError,
|
PageError,
|
||||||
@@ -761,6 +763,43 @@ export class ClaudeBrowser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleImportCommand(cmd: ImportCommand): Promise<CommandResponse> {
|
||||||
|
const context = this.getContext();
|
||||||
|
if (!context) throw new Error('Browser not launched');
|
||||||
|
|
||||||
|
if (cmd.source === 'safari') {
|
||||||
|
const cookies = await safari.importSafariCookies({
|
||||||
|
domain: cmd.domain,
|
||||||
|
profile: cmd.profile,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cookies.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
imported: 0,
|
||||||
|
source: 'safari',
|
||||||
|
domains: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to Playwright format and add to context
|
||||||
|
const playwrightCookies = cookies.map(safari.toPlaywrightCookie);
|
||||||
|
await context.addCookies(playwrightCookies);
|
||||||
|
|
||||||
|
// Get unique domains for reporting
|
||||||
|
const domains = [...new Set(cookies.map((c) => c.domain))];
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
imported: cookies.length,
|
||||||
|
source: 'safari',
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, error: `Unknown import source: ${cmd.source}` };
|
||||||
|
}
|
||||||
|
|
||||||
async executeCommand(cmd: BrowserCommand): Promise<CommandResponse> {
|
async executeCommand(cmd: BrowserCommand): Promise<CommandResponse> {
|
||||||
try {
|
try {
|
||||||
switch (cmd.cmd) {
|
switch (cmd.cmd) {
|
||||||
@@ -955,6 +994,8 @@ export class ClaudeBrowser {
|
|||||||
size: result.size,
|
size: result.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'import':
|
||||||
|
return this.handleImportCommand(cmd);
|
||||||
default: {
|
default: {
|
||||||
const _exhaustive: never = cmd;
|
const _exhaustive: never = cmd;
|
||||||
return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` };
|
return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` };
|
||||||
|
|||||||
+19
@@ -677,6 +677,25 @@ server.tool(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Browser import
|
||||||
|
server.tool(
|
||||||
|
'import',
|
||||||
|
'Import cookies from Safari browser (macOS only). Requires Full Disk Access permission.',
|
||||||
|
{
|
||||||
|
source: z.enum(['safari']).describe('Browser to import from (currently only Safari supported)'),
|
||||||
|
domain: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Filter cookies to specific domain (e.g., "github.com")'),
|
||||||
|
profile: z.string().optional().describe('Safari profile/WebKit data store ID (optional)'),
|
||||||
|
},
|
||||||
|
withLogging('import', async ({ source, domain, profile }) => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.executeCommand({ cmd: 'import', source, domain, profile });
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Image processing
|
// Image processing
|
||||||
server.tool(
|
server.tool(
|
||||||
'favicon',
|
'favicon',
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { type SafariCookie, toPlaywrightCookie } from './safari.js';
|
||||||
|
|
||||||
|
describe('safari', () => {
|
||||||
|
describe('toPlaywrightCookie', () => {
|
||||||
|
it('should convert SafariCookie to Playwright format', () => {
|
||||||
|
const safariCookie: SafariCookie = {
|
||||||
|
name: 'session_id',
|
||||||
|
value: 'abc123',
|
||||||
|
domain: '.example.com',
|
||||||
|
path: '/',
|
||||||
|
expires: 1735689600, // 2025-01-01
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = toPlaywrightCookie(safariCookie);
|
||||||
|
|
||||||
|
expect(result.name).toBe('session_id');
|
||||||
|
expect(result.value).toBe('abc123');
|
||||||
|
expect(result.domain).toBe('.example.com');
|
||||||
|
expect(result.path).toBe('/');
|
||||||
|
expect(result.expires).toBe(1735689600);
|
||||||
|
expect(result.secure).toBe(true);
|
||||||
|
expect(result.httpOnly).toBe(true);
|
||||||
|
expect(result.sameSite).toBe('None'); // Secure cookies get SameSite=None
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set SameSite to Lax for non-secure cookies', () => {
|
||||||
|
const safariCookie: SafariCookie = {
|
||||||
|
name: 'tracking',
|
||||||
|
value: 'xyz',
|
||||||
|
domain: 'example.com',
|
||||||
|
path: '/',
|
||||||
|
expires: 1735689600,
|
||||||
|
secure: false,
|
||||||
|
httpOnly: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = toPlaywrightCookie(safariCookie);
|
||||||
|
|
||||||
|
expect(result.secure).toBe(false);
|
||||||
|
expect(result.sameSite).toBe('Lax');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* Safari Cookies.binarycookies parser
|
||||||
|
*
|
||||||
|
* Format specification: https://github.com/libyal/dtformats/blob/main/documentation/Safari%20Cookies.asciidoc
|
||||||
|
*
|
||||||
|
* File structure:
|
||||||
|
* - Header: "cook" magic + page count + page sizes array
|
||||||
|
* - Pages: Each contains cookie records
|
||||||
|
* - Footer: 8 bytes (checksum)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
export interface SafariCookie {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
expires: number; // Unix timestamp (seconds)
|
||||||
|
secure: boolean;
|
||||||
|
httpOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cocoa epoch (Jan 1, 2001) to Unix epoch (Jan 1, 1970) offset in seconds
|
||||||
|
const COCOA_EPOCH_OFFSET = 978307200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Cocoa timestamp (seconds since Jan 1, 2001) to Unix timestamp
|
||||||
|
*/
|
||||||
|
function cocoaToUnix(cocoaTime: number): number {
|
||||||
|
return Math.floor(cocoaTime + COCOA_EPOCH_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a null-terminated string from buffer at offset
|
||||||
|
*/
|
||||||
|
function readCString(buffer: Buffer, offset: number): string {
|
||||||
|
let end = offset;
|
||||||
|
while (end < buffer.length && buffer[end] !== 0) {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
return buffer.subarray(offset, end).toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single cookie record from a page
|
||||||
|
*/
|
||||||
|
function parseCookieRecord(buffer: Buffer, recordOffset: number): SafariCookie {
|
||||||
|
// Cookie record structure (little-endian):
|
||||||
|
// 0-3: Record size
|
||||||
|
// 4-7: Unknown
|
||||||
|
// 8-11: Flags (1=secure, 4=httpOnly)
|
||||||
|
// 12-15: Unknown
|
||||||
|
// 16-19: URL/domain offset (relative to record start)
|
||||||
|
// 20-23: Name offset
|
||||||
|
// 24-27: Path offset
|
||||||
|
// 28-31: Value offset
|
||||||
|
// 32-39: Unknown (end marker)
|
||||||
|
// 40-47: Expiration (64-bit float, Cocoa timestamp)
|
||||||
|
// 48-55: Creation (64-bit float, Cocoa timestamp)
|
||||||
|
// 56+: String data
|
||||||
|
|
||||||
|
const flags = buffer.readUInt32LE(recordOffset + 8);
|
||||||
|
const domainOffset = buffer.readUInt32LE(recordOffset + 16);
|
||||||
|
const nameOffset = buffer.readUInt32LE(recordOffset + 20);
|
||||||
|
const pathOffset = buffer.readUInt32LE(recordOffset + 24);
|
||||||
|
const valueOffset = buffer.readUInt32LE(recordOffset + 28);
|
||||||
|
const expiration = buffer.readDoubleLE(recordOffset + 40);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: readCString(buffer, recordOffset + nameOffset),
|
||||||
|
value: readCString(buffer, recordOffset + valueOffset),
|
||||||
|
domain: readCString(buffer, recordOffset + domainOffset),
|
||||||
|
path: readCString(buffer, recordOffset + pathOffset),
|
||||||
|
expires: cocoaToUnix(expiration),
|
||||||
|
secure: (flags & 0x01) !== 0,
|
||||||
|
httpOnly: (flags & 0x04) !== 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a page of cookies
|
||||||
|
*/
|
||||||
|
function parsePage(buffer: Buffer): SafariCookie[] {
|
||||||
|
const cookies: SafariCookie[] = [];
|
||||||
|
|
||||||
|
// Page header:
|
||||||
|
// 0-3: Page signature (0x00000100 as big-endian)
|
||||||
|
// 4-7: Number of cookies (little-endian)
|
||||||
|
// 8+: Cookie record offsets array (little-endian)
|
||||||
|
|
||||||
|
const signature = buffer.readUInt32BE(0);
|
||||||
|
if (signature !== 0x00000100) {
|
||||||
|
// Invalid page signature, skip
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieCount = buffer.readUInt32LE(4);
|
||||||
|
|
||||||
|
for (let i = 0; i < cookieCount; i++) {
|
||||||
|
const recordOffset = buffer.readUInt32LE(8 + i * 4);
|
||||||
|
try {
|
||||||
|
const cookie = parseCookieRecord(buffer, recordOffset);
|
||||||
|
cookies.push(cookie);
|
||||||
|
} catch {
|
||||||
|
// Skip malformed cookie records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a Safari Cookies.binarycookies file
|
||||||
|
*/
|
||||||
|
export async function parseBinaryCookies(filePath: string): Promise<SafariCookie[]> {
|
||||||
|
const buffer = await readFile(filePath);
|
||||||
|
const cookies: SafariCookie[] = [];
|
||||||
|
|
||||||
|
// File header (big-endian):
|
||||||
|
// 0-3: Magic "cook"
|
||||||
|
// 4-7: Number of pages
|
||||||
|
// 8+: Page sizes array (4 bytes each)
|
||||||
|
|
||||||
|
const magic = buffer.subarray(0, 4).toString('ascii');
|
||||||
|
if (magic !== 'cook') {
|
||||||
|
throw new Error(`Invalid binarycookies file: expected "cook" magic, got "${magic}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCount = buffer.readUInt32BE(4);
|
||||||
|
const pageSizes: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
pageSizes.push(buffer.readUInt32BE(8 + i * 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate where pages start (after header)
|
||||||
|
let pageOffset = 8 + pageCount * 4;
|
||||||
|
|
||||||
|
// Parse each page
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
const pageSize = pageSizes[i];
|
||||||
|
const pageBuffer = buffer.subarray(pageOffset, pageOffset + pageSize);
|
||||||
|
const pageCookies = parsePage(pageBuffer);
|
||||||
|
cookies.push(...pageCookies);
|
||||||
|
pageOffset += pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default Safari cookies file path
|
||||||
|
*/
|
||||||
|
export function getSafariCookiesPath(profile?: string): string {
|
||||||
|
const home = homedir();
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
// Profile-specific WebKit data store
|
||||||
|
return join(
|
||||||
|
home,
|
||||||
|
'Library/Containers/com.apple.Safari/Data/Library/WebKit/WebsiteDataStore',
|
||||||
|
profile,
|
||||||
|
'Cookies/Cookies.binarycookies'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Safari cookies location
|
||||||
|
return join(
|
||||||
|
home,
|
||||||
|
'Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available Safari profiles (WebKit data stores)
|
||||||
|
*/
|
||||||
|
export async function listSafariProfiles(): Promise<string[]> {
|
||||||
|
const { readdir } = await import('node:fs/promises');
|
||||||
|
const home = homedir();
|
||||||
|
const webkitPath = join(
|
||||||
|
home,
|
||||||
|
'Library/Containers/com.apple.Safari/Data/Library/WebKit/WebsiteDataStore'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await readdir(webkitPath, { withFileTypes: true });
|
||||||
|
return entries
|
||||||
|
.filter((e) => e.isDirectory())
|
||||||
|
.map((e) => e.name)
|
||||||
|
.filter((name) => {
|
||||||
|
// Check if this profile has a cookies file
|
||||||
|
const cookiePath = join(webkitPath, name, 'Cookies/Cookies.binarycookies');
|
||||||
|
return existsSync(cookiePath);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import Safari cookies, optionally filtered by domain
|
||||||
|
*/
|
||||||
|
export async function importSafariCookies(options?: {
|
||||||
|
profile?: string;
|
||||||
|
domain?: string;
|
||||||
|
}): Promise<SafariCookie[]> {
|
||||||
|
const cookiesPath = getSafariCookiesPath(options?.profile);
|
||||||
|
|
||||||
|
if (!existsSync(cookiesPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Safari cookies file not found at: ${cookiesPath}\nMake sure Safari has been used and Full Disk Access is granted to your terminal.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookies = await parseBinaryCookies(cookiesPath);
|
||||||
|
|
||||||
|
// Filter by domain if specified
|
||||||
|
if (options?.domain) {
|
||||||
|
const domainFilter = options.domain.toLowerCase();
|
||||||
|
cookies = cookies.filter((c) => {
|
||||||
|
const cookieDomain = c.domain.toLowerCase();
|
||||||
|
// Match exact domain or subdomain (e.g., ".example.com" matches "sub.example.com")
|
||||||
|
return (
|
||||||
|
cookieDomain === domainFilter ||
|
||||||
|
cookieDomain === `.${domainFilter}` ||
|
||||||
|
cookieDomain.endsWith(`.${domainFilter}`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert SafariCookie to Playwright cookie format
|
||||||
|
*/
|
||||||
|
export function toPlaywrightCookie(cookie: SafariCookie): {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
expires: number;
|
||||||
|
secure: boolean;
|
||||||
|
httpOnly: boolean;
|
||||||
|
sameSite: 'Strict' | 'Lax' | 'None';
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
name: cookie.name,
|
||||||
|
value: cookie.value,
|
||||||
|
domain: cookie.domain,
|
||||||
|
path: cookie.path,
|
||||||
|
expires: cookie.expires,
|
||||||
|
secure: cookie.secure,
|
||||||
|
httpOnly: cookie.httpOnly,
|
||||||
|
sameSite: cookie.secure ? 'None' : 'Lax', // Best guess since Safari doesn't store this
|
||||||
|
};
|
||||||
|
}
|
||||||
+14
-1
@@ -301,6 +301,14 @@ export interface EmulateCommand {
|
|||||||
device: string;
|
device: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safari import
|
||||||
|
export interface ImportCommand {
|
||||||
|
cmd: 'import';
|
||||||
|
source: 'safari';
|
||||||
|
domain?: string;
|
||||||
|
profile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type BrowserCommand =
|
export type BrowserCommand =
|
||||||
| GotoCommand
|
| GotoCommand
|
||||||
| ClickCommand
|
| ClickCommand
|
||||||
@@ -337,7 +345,8 @@ export type BrowserCommand =
|
|||||||
| UploadCommand
|
| UploadCommand
|
||||||
| ScrollCommand
|
| ScrollCommand
|
||||||
| ViewportCommand
|
| ViewportCommand
|
||||||
| EmulateCommand;
|
| EmulateCommand
|
||||||
|
| ImportCommand;
|
||||||
|
|
||||||
// Response types
|
// Response types
|
||||||
export interface SuccessResponse {
|
export interface SuccessResponse {
|
||||||
@@ -378,6 +387,10 @@ export interface SuccessResponse {
|
|||||||
viewport?: { width: number; height: number };
|
viewport?: { width: number; height: number };
|
||||||
// Selected values
|
// Selected values
|
||||||
selected?: string[];
|
selected?: string[];
|
||||||
|
// Import fields
|
||||||
|
imported?: number;
|
||||||
|
source?: string;
|
||||||
|
domains?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user