Skip to content

Commit efff7af

Browse files
authored
Merge pull request #17 from scrapeless-ai/feat/browser_extension
Feat/browser extension
2 parents 2540bad + f73db23 commit efff7af

File tree

7 files changed

+232
-1
lines changed

7 files changed

+232
-1
lines changed

examples/extension-example.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Scrapeless } from '@scrapeless-ai/sdk';
2+
import puppeteer from 'puppeteer-core';
3+
4+
const client = new Scrapeless();
5+
const { browser: scrapingBrowser } = client;
6+
7+
async function uploadExtension() {
8+
const response = await scrapingBrowser.extension.upload('your-file-path.zip', 'Scrapeless');
9+
console.log(response);
10+
}
11+
12+
async function updateExtension(extensionId) {
13+
const response = await scrapingBrowser.extension.update(extensionId, 'your-file-path.zip', 'Scrapeless');
14+
console.log(response);
15+
}
16+
17+
async function listExtensions() {
18+
const response = await scrapingBrowser.extension.list();
19+
console.log(response);
20+
}
21+
22+
async function deleteExtension(extensionId) {
23+
const response = await scrapingBrowser.extension.delete(extensionId);
24+
console.log(response);
25+
}
26+
27+
async function getExtension(extensionId) {
28+
const response = await scrapingBrowser.extension.get(extensionId);
29+
console.log(response);
30+
}
31+
32+
async function useExtension(extensionIds) {
33+
const { browserWSEndpoint } = scrapingBrowser.create({
34+
session_name: 'use-extension',
35+
session_ttl: 180,
36+
session_recording: true,
37+
extension_ids: extensionIds
38+
});
39+
40+
const browser = await puppeteer.connect({
41+
browserWSEndpoint,
42+
defaultViewport: null
43+
});
44+
45+
const page = await browser.newPage();
46+
await page.goto('https://example.com');
47+
}
48+
49+
async function runExtensionTests() {
50+
try {
51+
console.log('=== Scrapeless Browser Extension Tests ===\n');
52+
53+
// Upload a new extension
54+
await uploadExtension();
55+
console.log('\n');
56+
57+
// List all extensions
58+
await listExtensions();
59+
console.log('\n');
60+
61+
// Get details of a specific extension (replace with actual ID)
62+
const extensionId = 'your-extension-id-here'; // Replace with actual extension ID
63+
await getExtension(extensionId);
64+
console.log('\n');
65+
66+
// Update the extension (replace with actual ID)
67+
await updateExtension(extensionId);
68+
console.log('\n');
69+
70+
// Delete the extension (replace with actual ID)
71+
await deleteExtension(extensionId);
72+
console.log('\n');
73+
74+
// Use the extension in a new browser session
75+
await useExtension(extensionId);
76+
console.log('\n');
77+
78+
console.log('🎉 All extension tests completed successfully');
79+
} catch (error) {
80+
console.error('❌ Extension tests failed with error:', error);
81+
}
82+
}
83+
84+
// Run the extension tests
85+
runExtensionTests().catch(console.error);

src/services/base.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ export abstract class BaseService {
3737
// log.debug("Request body:", options.body);
3838
}
3939

40+
if (body && additionalHeaders['content-type'].startsWith('multipart/form-data;')) {
41+
options.headers = {
42+
...additionalHeaders,
43+
'X-API-Key': this.apiKey
44+
};
45+
options.body = body;
46+
}
47+
4048
const response = await nodeFetch(`${this.baseUrl}${endpoint}`, options);
4149

4250
// Get response content, parse it if it's JSON

src/services/browser.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { BaseService } from './base';
2+
import { ExtensionService } from './extension';
3+
import { getEnvWithDefault } from '../env';
24
import { ICreateBrowser, ICreateBrowserResponse } from '../types';
35

46
// Define default parameters
@@ -9,8 +11,13 @@ const DEFAULT_BROWSER_OPTIONS: ICreateBrowser = {
911
};
1012

1113
export class BrowserService extends BaseService {
14+
public readonly extension: ExtensionService;
15+
1216
constructor(apiKey: string, baseUrl: string, timeout: number = 30_000) {
1317
super(apiKey, baseUrl, timeout);
18+
19+
const baseApiURL = getEnvWithDefault('SCRAPELESS_BASE_API_URL', 'https://api.scrapeless.com');
20+
this.extension = new ExtensionService(apiKey, baseApiURL, timeout);
1421
}
1522

1623
/**
@@ -30,7 +37,8 @@ export class BrowserService extends BaseService {
3037
session_recording: data.session_recording?.toString(),
3138
proxy_country: data.proxy_country,
3239
proxy_url: data.proxy_url,
33-
fingerprint: data.fingerprint ? JSON.stringify(data.fingerprint) : undefined
40+
fingerprint: data.fingerprint ? JSON.stringify(data.fingerprint) : undefined,
41+
extension_ids: data.extension_ids
3442
};
3543

3644
if (data.proxy_url) {

src/services/extension.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import fs from 'node:fs';
2+
import FormData from 'form-data';
3+
import { BaseService } from './base';
4+
5+
import { BrowserExtension, UploadExtensionResponse, ExtensionDetail, ExtensionListItem } from '../types';
6+
7+
export class ExtensionService extends BaseService implements BrowserExtension {
8+
constructor(apiKey: string, baseUrl: string, timeout: number) {
9+
super(apiKey, baseUrl, timeout);
10+
}
11+
12+
/**
13+
* Validates the file path and extracts the file name.
14+
* @param filePath The path to the file.
15+
* @returns The extracted file name.
16+
* @throws Error if the file suffix is not valid.
17+
*/
18+
private getFileName(filePath: string): string {
19+
const validSuffixes = ['.zip'];
20+
const fileSuffix = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
21+
if (!validSuffixes.includes(fileSuffix)) {
22+
throw new Error(`Invalid file suffix: ${fileSuffix}. Supported suffixes: ${validSuffixes.join(', ')}`);
23+
}
24+
25+
return filePath.slice(filePath.lastIndexOf('/') + 1);
26+
}
27+
28+
async upload(filePath: string, name: string): Promise<UploadExtensionResponse> {
29+
const fileName = this.getFileName(filePath);
30+
const fileStream = fs.createReadStream(filePath);
31+
32+
const formData = new FormData();
33+
formData.append('file', fileStream, fileName);
34+
formData.append('name', name);
35+
36+
const res = await this.request<UploadExtensionResponse, true>(
37+
'/browser/extensions/upload',
38+
'POST',
39+
formData,
40+
formData.getHeaders(),
41+
true
42+
);
43+
44+
return res.data;
45+
}
46+
47+
async update(extensionId: string, filePath: string, name?: string): Promise<{ success: boolean }> {
48+
const fileName = this.getFileName(filePath);
49+
const fileStream = fs.createReadStream(filePath);
50+
51+
const formData = new FormData();
52+
formData.append('file', fileStream, fileName);
53+
if (name) {
54+
formData.append('name', name);
55+
}
56+
57+
const res = await this.request<{ success: boolean }, true>(
58+
`/browser/extensions/${extensionId}`,
59+
'PUT',
60+
formData,
61+
formData.getHeaders(),
62+
true
63+
);
64+
65+
return res.data;
66+
}
67+
68+
async get(extensionId: string): Promise<ExtensionDetail> {
69+
const res = await this.request<ExtensionDetail, true>(
70+
`/browser/extensions/${extensionId}`,
71+
'GET',
72+
undefined,
73+
{},
74+
true
75+
);
76+
77+
return res.data;
78+
}
79+
80+
async list(): Promise<ExtensionListItem[]> {
81+
const res = await this.request<ExtensionListItem[], true>('/browser/extensions/list', 'GET', undefined, {}, true);
82+
return res.data;
83+
}
84+
85+
async delete(extensionId: string): Promise<{ success: boolean }> {
86+
const res = await this.request<{ success: boolean }, true>(
87+
`/browser/extensions/${extensionId}`,
88+
'DELETE',
89+
undefined,
90+
{},
91+
true
92+
);
93+
94+
return res.data;
95+
}
96+
}

src/types/browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface ICreateBrowser {
1414
proxy_country?: string;
1515
proxy_url?: string;
1616
fingerprint?: object;
17+
extension_ids?: string;
1718
}
1819

1920
/**

src/types/extension.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export interface UploadExtensionResponse {
2+
extensionId: string;
3+
name: string;
4+
createdAt: Date;
5+
updatedAt: Date;
6+
}
7+
8+
export interface ExtensionListItem {
9+
extensionId: string;
10+
name: string;
11+
version: string;
12+
createdAt: Date;
13+
updatedAt: Date;
14+
}
15+
16+
export interface ExtensionDetail {
17+
extensionId: string;
18+
teamId: string;
19+
manifestName: string;
20+
name: string;
21+
version: string;
22+
createdAt: Date;
23+
updatedAt: Date;
24+
}
25+
26+
export interface BrowserExtension {
27+
upload: (filePath: string, name: string) => Promise<UploadExtensionResponse>;
28+
update: (extensionId: string, filePath: string, name?: string) => Promise<{ success: boolean }>;
29+
get: (extensionId: string) => Promise<ExtensionDetail>;
30+
list: () => Promise<ExtensionListItem[]>;
31+
delete: (extensionId: string) => Promise<{ success: boolean }>;
32+
}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './actor';
1111
export * from './runner';
1212
export * from './storage';
1313
export * from './scraping-crawl';
14+
export * from './extension';

0 commit comments

Comments
 (0)