@web-agent-proxy/sdk-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto-helper.d.ts +9 -0
- package/dist/crypto-helper.d.ts.map +1 -0
- package/dist/crypto-helper.js +26 -0
- package/dist/crypto-helper.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/index.js.map +1 -0
- package/package.json +15 -0
- package/src/crypto-helper.ts +45 -0
- package/src/index.ts +147 -0
- package/test/client.test.mjs +153 -0
- package/test/crypto-helper.test.mjs +35 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function deriveKey(passphrase: string): Buffer;
|
|
2
|
+
export interface EncryptedPayload {
|
|
3
|
+
iv: string;
|
|
4
|
+
tag: string;
|
|
5
|
+
data: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function encryptPayload(plaintext: string, key: Buffer): EncryptedPayload;
|
|
8
|
+
export declare function decryptPayload(ciphertext: string, key: Buffer, ivBase64: string, tagBase64: string): string;
|
|
9
|
+
//# sourceMappingURL=crypto-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-helper.d.ts","sourceRoot":"","sources":["../src/crypto-helper.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAc/E;AAED,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,CAWR"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
export function deriveKey(passphrase) {
|
|
3
|
+
return crypto.createHash("sha256").update(passphrase).digest();
|
|
4
|
+
}
|
|
5
|
+
export function encryptPayload(plaintext, key) {
|
|
6
|
+
const iv = crypto.randomBytes(12);
|
|
7
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
8
|
+
let encrypted = cipher.update(plaintext, "utf8", "base64");
|
|
9
|
+
encrypted += cipher.final("base64");
|
|
10
|
+
const tag = cipher.getAuthTag().toString("base64");
|
|
11
|
+
return {
|
|
12
|
+
iv: iv.toString("base64"),
|
|
13
|
+
tag: tag,
|
|
14
|
+
data: encrypted,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function decryptPayload(ciphertext, key, ivBase64, tagBase64) {
|
|
18
|
+
const iv = Buffer.from(ivBase64, "base64");
|
|
19
|
+
const tag = Buffer.from(tagBase64, "base64");
|
|
20
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
21
|
+
decipher.setAuthTag(tag);
|
|
22
|
+
let decrypted = decipher.update(ciphertext, "base64", "utf8");
|
|
23
|
+
decrypted += decipher.final("utf8");
|
|
24
|
+
return decrypted;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=crypto-helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-helper.js","sourceRoot":"","sources":["../src/crypto-helper.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,UAAU,SAAS,CAAC,UAAkB;IAC1C,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,EAAE,CAAC;AACjE,CAAC;AAQD,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,GAAW;IAC3D,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAE7D,IAAI,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC3D,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEpC,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEnD,OAAO;QACL,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACzB,GAAG,EAAE,GAAG;QACR,IAAI,EAAE,SAAS;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,UAAkB,EAClB,GAAW,EACX,QAAgB,EAChB,SAAiB;IAEjB,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAE7C,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACjE,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAEzB,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9D,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEpC,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface ProxyRequestOptions {
|
|
2
|
+
action?: "scrape" | "screenshot";
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
cookies?: any[];
|
|
6
|
+
}
|
|
7
|
+
export interface ProxyResponse {
|
|
8
|
+
status: number;
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
content?: string;
|
|
11
|
+
screenshot?: string;
|
|
12
|
+
cookies?: any[];
|
|
13
|
+
}
|
|
14
|
+
export interface BrowserClientInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
metadata: {
|
|
17
|
+
name: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
};
|
|
20
|
+
status: "idle" | "busy";
|
|
21
|
+
}
|
|
22
|
+
export declare class WebAgentProxyClient {
|
|
23
|
+
private bridgeUrl;
|
|
24
|
+
private defaultClientId?;
|
|
25
|
+
private encryptionKey?;
|
|
26
|
+
private apiKey?;
|
|
27
|
+
constructor(options?: {
|
|
28
|
+
bridgeUrl?: string;
|
|
29
|
+
defaultClientId?: string;
|
|
30
|
+
encryptionKey?: string;
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
});
|
|
33
|
+
private getAuthHeaders;
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves a list of all active browser clients connected to the bridge.
|
|
36
|
+
*/
|
|
37
|
+
getBrowsers(): Promise<BrowserClientInfo[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Routes a web request through a registered browser client.
|
|
40
|
+
*/
|
|
41
|
+
request(url: string, options?: ProxyRequestOptions & {
|
|
42
|
+
clientId?: string;
|
|
43
|
+
}): Promise<ProxyResponse>;
|
|
44
|
+
/**
|
|
45
|
+
* Helper method to scrape the raw HTML of a web page.
|
|
46
|
+
*/
|
|
47
|
+
scrape(url: string, options?: Omit<ProxyRequestOptions, "action">): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Helper method to capture a screenshot of a web page (returns base64 data URI).
|
|
50
|
+
*/
|
|
51
|
+
captureScreenshot(url: string, options?: Omit<ProxyRequestOptions, "action">): Promise<string>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAQnH,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAUjD;;OAEG;IACG,OAAO,CACX,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,mBAAmB,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GACxD,OAAO,CAAC,aAAa,CAAC;IA6DzB;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,IAAI,CAAC,mBAAmB,EAAE,QAAQ,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK7F;;OAEG;IACG,iBAAiB,CACrB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,IAAI,CAAC,mBAAmB,EAAE,QAAQ,CAAM,GAChD,OAAO,CAAC,MAAM,CAAC;CAInB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { deriveKey, encryptPayload, decryptPayload } from "./crypto-helper.js";
|
|
2
|
+
export class WebAgentProxyClient {
|
|
3
|
+
bridgeUrl;
|
|
4
|
+
defaultClientId;
|
|
5
|
+
encryptionKey;
|
|
6
|
+
apiKey;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
// Default to localhost bridge
|
|
9
|
+
this.bridgeUrl = (options.bridgeUrl || "http://localhost:8080").replace(/\/$/, "");
|
|
10
|
+
this.defaultClientId = options.defaultClientId;
|
|
11
|
+
this.encryptionKey = options.encryptionKey;
|
|
12
|
+
this.apiKey = options.apiKey;
|
|
13
|
+
}
|
|
14
|
+
getAuthHeaders() {
|
|
15
|
+
const headers = { "Content-Type": "application/json" };
|
|
16
|
+
if (this.apiKey) {
|
|
17
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
18
|
+
}
|
|
19
|
+
return headers;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Retrieves a list of all active browser clients connected to the bridge.
|
|
23
|
+
*/
|
|
24
|
+
async getBrowsers() {
|
|
25
|
+
const response = await fetch(`${this.bridgeUrl}/api/browsers`, {
|
|
26
|
+
headers: this.getAuthHeaders(),
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Failed to fetch active browsers: ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
return response.json();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Routes a web request through a registered browser client.
|
|
35
|
+
*/
|
|
36
|
+
async request(url, options = {}) {
|
|
37
|
+
const clientId = options.clientId || this.defaultClientId || "auto";
|
|
38
|
+
let requestBody = {
|
|
39
|
+
clientId,
|
|
40
|
+
url,
|
|
41
|
+
};
|
|
42
|
+
const action = options.action || "scrape";
|
|
43
|
+
const method = options.method || "GET";
|
|
44
|
+
const headers = options.headers || {};
|
|
45
|
+
const cookies = options.cookies || [];
|
|
46
|
+
if (this.encryptionKey) {
|
|
47
|
+
const key = deriveKey(this.encryptionKey);
|
|
48
|
+
const plaintextPayload = JSON.stringify({ url, action, method, headers, cookies });
|
|
49
|
+
const encrypted = encryptPayload(plaintextPayload, key);
|
|
50
|
+
requestBody.url = "[Encrypted]";
|
|
51
|
+
requestBody.encrypted = true;
|
|
52
|
+
requestBody.iv = encrypted.iv;
|
|
53
|
+
requestBody.tag = encrypted.tag;
|
|
54
|
+
requestBody.data = encrypted.data;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
requestBody.url = url;
|
|
58
|
+
requestBody.action = action;
|
|
59
|
+
requestBody.method = method;
|
|
60
|
+
requestBody.headers = headers;
|
|
61
|
+
requestBody.cookies = cookies;
|
|
62
|
+
}
|
|
63
|
+
const response = await fetch(`${this.bridgeUrl}/api/request`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: this.getAuthHeaders(),
|
|
66
|
+
body: JSON.stringify(requestBody),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
70
|
+
throw new Error(errorData.error || `Proxy Request Failed: ${response.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
const responseData = (await response.json());
|
|
73
|
+
if (responseData.encrypted) {
|
|
74
|
+
if (!this.encryptionKey) {
|
|
75
|
+
throw new Error("Received encrypted response, but no encryptionKey is configured.");
|
|
76
|
+
}
|
|
77
|
+
const key = deriveKey(this.encryptionKey);
|
|
78
|
+
const decryptedJson = decryptPayload(responseData.data, key, responseData.iv, responseData.tag);
|
|
79
|
+
return JSON.parse(decryptedJson);
|
|
80
|
+
}
|
|
81
|
+
return responseData;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Helper method to scrape the raw HTML of a web page.
|
|
85
|
+
*/
|
|
86
|
+
async scrape(url, options = {}) {
|
|
87
|
+
const res = await this.request(url, { ...options, action: "scrape" });
|
|
88
|
+
return res.content || "";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Helper method to capture a screenshot of a web page (returns base64 data URI).
|
|
92
|
+
*/
|
|
93
|
+
async captureScreenshot(url, options = {}) {
|
|
94
|
+
const res = await this.request(url, { ...options, action: "screenshot" });
|
|
95
|
+
return res.screenshot || "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@web-agent-proxy/sdk-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JavaScript/TypeScript SDK client for Web Agent Proxy",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "npm run build && node --test \"test/*.test.mjs\"",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"typescript": "^5.3.3"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
export function deriveKey(passphrase: string): Buffer {
|
|
4
|
+
return crypto.createHash("sha256").update(passphrase).digest();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface EncryptedPayload {
|
|
8
|
+
iv: string;
|
|
9
|
+
tag: string;
|
|
10
|
+
data: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function encryptPayload(plaintext: string, key: Buffer): EncryptedPayload {
|
|
14
|
+
const iv = crypto.randomBytes(12);
|
|
15
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
16
|
+
|
|
17
|
+
let encrypted = cipher.update(plaintext, "utf8", "base64");
|
|
18
|
+
encrypted += cipher.final("base64");
|
|
19
|
+
|
|
20
|
+
const tag = cipher.getAuthTag().toString("base64");
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
iv: iv.toString("base64"),
|
|
24
|
+
tag: tag,
|
|
25
|
+
data: encrypted,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decryptPayload(
|
|
30
|
+
ciphertext: string,
|
|
31
|
+
key: Buffer,
|
|
32
|
+
ivBase64: string,
|
|
33
|
+
tagBase64: string
|
|
34
|
+
): string {
|
|
35
|
+
const iv = Buffer.from(ivBase64, "base64");
|
|
36
|
+
const tag = Buffer.from(tagBase64, "base64");
|
|
37
|
+
|
|
38
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
39
|
+
decipher.setAuthTag(tag);
|
|
40
|
+
|
|
41
|
+
let decrypted = decipher.update(ciphertext, "base64", "utf8");
|
|
42
|
+
decrypted += decipher.final("utf8");
|
|
43
|
+
|
|
44
|
+
return decrypted;
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { deriveKey, encryptPayload, decryptPayload } from "./crypto-helper.js";
|
|
2
|
+
|
|
3
|
+
export interface ProxyRequestOptions {
|
|
4
|
+
action?: "scrape" | "screenshot";
|
|
5
|
+
method?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
cookies?: any[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProxyResponse {
|
|
11
|
+
status: number;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
content?: string;
|
|
14
|
+
screenshot?: string;
|
|
15
|
+
cookies?: any[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BrowserClientInfo {
|
|
19
|
+
id: string;
|
|
20
|
+
metadata: {
|
|
21
|
+
name: string;
|
|
22
|
+
platform: string;
|
|
23
|
+
};
|
|
24
|
+
status: "idle" | "busy";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class WebAgentProxyClient {
|
|
28
|
+
private bridgeUrl: string;
|
|
29
|
+
private defaultClientId?: string;
|
|
30
|
+
private encryptionKey?: string;
|
|
31
|
+
private apiKey?: string;
|
|
32
|
+
|
|
33
|
+
constructor(options: { bridgeUrl?: string; defaultClientId?: string; encryptionKey?: string; apiKey?: string } = {}) {
|
|
34
|
+
// Default to localhost bridge
|
|
35
|
+
this.bridgeUrl = (options.bridgeUrl || "http://localhost:8080").replace(/\/$/, "");
|
|
36
|
+
this.defaultClientId = options.defaultClientId;
|
|
37
|
+
this.encryptionKey = options.encryptionKey;
|
|
38
|
+
this.apiKey = options.apiKey;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private getAuthHeaders(): Record<string, string> {
|
|
42
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
43
|
+
if (this.apiKey) {
|
|
44
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves a list of all active browser clients connected to the bridge.
|
|
51
|
+
*/
|
|
52
|
+
async getBrowsers(): Promise<BrowserClientInfo[]> {
|
|
53
|
+
const response = await fetch(`${this.bridgeUrl}/api/browsers`, {
|
|
54
|
+
headers: this.getAuthHeaders(),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Failed to fetch active browsers: ${response.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
return response.json() as Promise<BrowserClientInfo[]>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Routes a web request through a registered browser client.
|
|
64
|
+
*/
|
|
65
|
+
async request(
|
|
66
|
+
url: string,
|
|
67
|
+
options: ProxyRequestOptions & { clientId?: string } = {}
|
|
68
|
+
): Promise<ProxyResponse> {
|
|
69
|
+
const clientId = options.clientId || this.defaultClientId || "auto";
|
|
70
|
+
|
|
71
|
+
let requestBody: any = {
|
|
72
|
+
clientId,
|
|
73
|
+
url,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const action = options.action || "scrape";
|
|
77
|
+
const method = options.method || "GET";
|
|
78
|
+
const headers = options.headers || {};
|
|
79
|
+
const cookies = options.cookies || [];
|
|
80
|
+
|
|
81
|
+
if (this.encryptionKey) {
|
|
82
|
+
const key = deriveKey(this.encryptionKey);
|
|
83
|
+
const plaintextPayload = JSON.stringify({ url, action, method, headers, cookies });
|
|
84
|
+
const encrypted = encryptPayload(plaintextPayload, key);
|
|
85
|
+
|
|
86
|
+
requestBody.url = "[Encrypted]";
|
|
87
|
+
requestBody.encrypted = true;
|
|
88
|
+
requestBody.iv = encrypted.iv;
|
|
89
|
+
requestBody.tag = encrypted.tag;
|
|
90
|
+
requestBody.data = encrypted.data;
|
|
91
|
+
} else {
|
|
92
|
+
requestBody.url = url;
|
|
93
|
+
requestBody.action = action;
|
|
94
|
+
requestBody.method = method;
|
|
95
|
+
requestBody.headers = headers;
|
|
96
|
+
requestBody.cookies = cookies;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const response = await fetch(`${this.bridgeUrl}/api/request`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: this.getAuthHeaders(),
|
|
102
|
+
body: JSON.stringify(requestBody),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorData = (await response.json().catch(() => ({}))) as any;
|
|
107
|
+
throw new Error(errorData.error || `Proxy Request Failed: ${response.statusText}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const responseData = (await response.json()) as any;
|
|
111
|
+
|
|
112
|
+
if (responseData.encrypted) {
|
|
113
|
+
if (!this.encryptionKey) {
|
|
114
|
+
throw new Error("Received encrypted response, but no encryptionKey is configured.");
|
|
115
|
+
}
|
|
116
|
+
const key = deriveKey(this.encryptionKey);
|
|
117
|
+
const decryptedJson = decryptPayload(
|
|
118
|
+
responseData.data,
|
|
119
|
+
key,
|
|
120
|
+
responseData.iv,
|
|
121
|
+
responseData.tag
|
|
122
|
+
);
|
|
123
|
+
return JSON.parse(decryptedJson) as ProxyResponse;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return responseData as ProxyResponse;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Helper method to scrape the raw HTML of a web page.
|
|
131
|
+
*/
|
|
132
|
+
async scrape(url: string, options: Omit<ProxyRequestOptions, "action"> = {}): Promise<string> {
|
|
133
|
+
const res = await this.request(url, { ...options, action: "scrape" });
|
|
134
|
+
return res.content || "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Helper method to capture a screenshot of a web page (returns base64 data URI).
|
|
139
|
+
*/
|
|
140
|
+
async captureScreenshot(
|
|
141
|
+
url: string,
|
|
142
|
+
options: Omit<ProxyRequestOptions, "action"> = {}
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
const res = await this.request(url, { ...options, action: "screenshot" });
|
|
145
|
+
return res.screenshot || "";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { decryptPayload, deriveKey, encryptPayload } from "../dist/crypto-helper.js";
|
|
4
|
+
import { WebAgentProxyClient } from "../dist/index.js";
|
|
5
|
+
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
8
|
+
test.after(() => {
|
|
9
|
+
globalThis.fetch = originalFetch;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("getBrowsers sends API key auth and parses browser metadata", async () => {
|
|
13
|
+
globalThis.fetch = async (input, init = {}) => {
|
|
14
|
+
assert.equal(input, "http://bridge.test/api/browsers");
|
|
15
|
+
assert.equal(init.headers.Authorization, "Bearer test-key");
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
statusText: "OK",
|
|
20
|
+
json: async () => [
|
|
21
|
+
{
|
|
22
|
+
id: "br-a",
|
|
23
|
+
metadata: { name: "Local", platform: "desktop" },
|
|
24
|
+
status: "idle",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const client = new WebAgentProxyClient({
|
|
31
|
+
bridgeUrl: "http://bridge.test/",
|
|
32
|
+
apiKey: "test-key",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const browsers = await client.getBrowsers();
|
|
36
|
+
assert.equal(browsers[0].id, "br-a");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("request sends the plain SDK contract", async () => {
|
|
40
|
+
const cookies = [{ name: "session", value: "abc123", domain: ".example.com" }];
|
|
41
|
+
|
|
42
|
+
globalThis.fetch = async (input, init = {}) => {
|
|
43
|
+
assert.equal(input, "http://bridge.test/api/request");
|
|
44
|
+
assert.equal(init.method, "POST");
|
|
45
|
+
assert.equal(init.headers.Authorization, "Bearer test-key");
|
|
46
|
+
|
|
47
|
+
const body = JSON.parse(init.body);
|
|
48
|
+
assert.deepEqual(body, {
|
|
49
|
+
clientId: "br-default",
|
|
50
|
+
url: "https://example.com",
|
|
51
|
+
action: "scrape",
|
|
52
|
+
method: "GET",
|
|
53
|
+
headers: { "Accept-Language": "en-US" },
|
|
54
|
+
cookies,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
statusText: "OK",
|
|
60
|
+
json: async () => ({
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { "content-type": "text/html" },
|
|
63
|
+
content: "<html>ok</html>",
|
|
64
|
+
cookies: [],
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const client = new WebAgentProxyClient({
|
|
70
|
+
bridgeUrl: "http://bridge.test/",
|
|
71
|
+
defaultClientId: "br-default",
|
|
72
|
+
apiKey: "test-key",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await client.request("https://example.com", {
|
|
76
|
+
headers: { "Accept-Language": "en-US" },
|
|
77
|
+
cookies,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(result.status, 200);
|
|
81
|
+
assert.equal(result.content, "<html>ok</html>");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("request encrypts payloads and decrypts encrypted responses", async () => {
|
|
85
|
+
const clearUrl = "https://example.com/private";
|
|
86
|
+
const key = deriveKey("shared-secret");
|
|
87
|
+
|
|
88
|
+
globalThis.fetch = async (_input, init = {}) => {
|
|
89
|
+
const bodyText = init.body;
|
|
90
|
+
const body = JSON.parse(bodyText);
|
|
91
|
+
|
|
92
|
+
assert.equal(body.clientId, "auto");
|
|
93
|
+
assert.equal(body.url, "[Encrypted]");
|
|
94
|
+
assert.equal(body.encrypted, true);
|
|
95
|
+
assert.equal(bodyText.includes(clearUrl), false);
|
|
96
|
+
|
|
97
|
+
const decryptedRequest = JSON.parse(decryptPayload(body.data, key, body.iv, body.tag));
|
|
98
|
+
assert.deepEqual(decryptedRequest, {
|
|
99
|
+
url: clearUrl,
|
|
100
|
+
action: "screenshot",
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "X-Test": "yes" },
|
|
103
|
+
cookies: [],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const encryptedResponse = encryptPayload(
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
status: 201,
|
|
109
|
+
headers: {},
|
|
110
|
+
screenshot: "data:image/png;base64,abc",
|
|
111
|
+
}),
|
|
112
|
+
key
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
statusText: "OK",
|
|
118
|
+
json: async () => ({
|
|
119
|
+
encrypted: true,
|
|
120
|
+
...encryptedResponse,
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const client = new WebAgentProxyClient({
|
|
126
|
+
bridgeUrl: "http://bridge.test",
|
|
127
|
+
encryptionKey: "shared-secret",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await client.request(clearUrl, {
|
|
131
|
+
action: "screenshot",
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "X-Test": "yes" },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
assert.equal(result.status, 201);
|
|
137
|
+
assert.equal(result.screenshot, "data:image/png;base64,abc");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("request surfaces bridge error payloads", async () => {
|
|
141
|
+
globalThis.fetch = async () => ({
|
|
142
|
+
ok: false,
|
|
143
|
+
statusText: "Gateway Timeout",
|
|
144
|
+
json: async () => ({ error: "Gateway Timeout (Queue Timeout 60s)" }),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const client = new WebAgentProxyClient({ bridgeUrl: "http://bridge.test" });
|
|
148
|
+
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
() => client.request("https://example.com"),
|
|
151
|
+
/Queue Timeout 60s/
|
|
152
|
+
);
|
|
153
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { decryptPayload, deriveKey, encryptPayload } from "../dist/crypto-helper.js";
|
|
4
|
+
|
|
5
|
+
test("deriveKey returns a deterministic AES-256 key", () => {
|
|
6
|
+
const keyA = deriveKey("shared-secret");
|
|
7
|
+
const keyB = deriveKey("shared-secret");
|
|
8
|
+
|
|
9
|
+
assert.equal(keyA.length, 32);
|
|
10
|
+
assert.deepEqual(keyA, keyB);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("encryptPayload and decryptPayload round-trip JSON payloads", () => {
|
|
14
|
+
const key = deriveKey("shared-secret");
|
|
15
|
+
const plaintext = JSON.stringify({
|
|
16
|
+
url: "https://example.com/report",
|
|
17
|
+
headers: { "Accept-Language": "en-US" },
|
|
18
|
+
cookies: [{ name: "session", value: "abc123" }],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const encrypted = encryptPayload(plaintext, key);
|
|
22
|
+
|
|
23
|
+
assert.notEqual(encrypted.data, plaintext);
|
|
24
|
+
assert.equal(decryptPayload(encrypted.data, key, encrypted.iv, encrypted.tag), plaintext);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("decryptPayload rejects tampered ciphertext", () => {
|
|
28
|
+
const key = deriveKey("shared-secret");
|
|
29
|
+
const encrypted = encryptPayload("sensitive payload", key);
|
|
30
|
+
const tamperedData = `${encrypted.data.slice(0, -2)}AA`;
|
|
31
|
+
|
|
32
|
+
assert.throws(() => {
|
|
33
|
+
decryptPayload(tamperedData, key, encrypted.iv, encrypted.tag);
|
|
34
|
+
}, Error);
|
|
35
|
+
});
|