@vibetechnologies/chrome-sync 0.4.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/README.md +78 -0
- package/dist/api.d.ts +46 -0
- package/dist/api.js +152 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +133 -0
- package/dist/cli.js.map +1 -0
- package/dist/extract.d.ts +40 -0
- package/dist/extract.js +228 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +87 -0
- package/dist/server.js.map +1 -0
- package/package.json +42 -0
- package/src/api.ts +189 -0
- package/src/cli.ts +162 -0
- package/src/extract.ts +321 -0
- package/src/index.ts +3 -0
- package/src/server.ts +116 -0
- package/tsconfig.json +15 -0
package/dist/extract.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome cookie extraction — reads and decrypts cookies from local Chrome profile.
|
|
3
|
+
*
|
|
4
|
+
* Supports: macOS (Keychain), Linux (libsecret / fallback "peanuts"), Windows (DPAPI via AES-GCM).
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { copyFileSync, existsSync, mkdtempSync } from "node:fs";
|
|
8
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
/** Get Chrome profile directory for current OS */
|
|
12
|
+
export function getChromeProfileDir(profile = "Default") {
|
|
13
|
+
const home = homedir();
|
|
14
|
+
switch (platform()) {
|
|
15
|
+
case "darwin":
|
|
16
|
+
return join(home, "Library/Application Support/Google/Chrome", profile);
|
|
17
|
+
case "linux":
|
|
18
|
+
return join(home, ".config/google-chrome", profile);
|
|
19
|
+
case "win32":
|
|
20
|
+
return join(process.env.LOCALAPPDATA || join(home, "AppData/Local"), "Google/Chrome/User Data", profile);
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(`Unsupported platform: ${platform()}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Get Chrome cookie DB path */
|
|
26
|
+
export function getCookieDbPath(profile = "Default") {
|
|
27
|
+
const profileDir = getChromeProfileDir(profile);
|
|
28
|
+
// Windows stores in Network/ subfolder since Chrome 96+
|
|
29
|
+
if (platform() === "win32") {
|
|
30
|
+
const networkPath = join(profileDir, "Network", "Cookies");
|
|
31
|
+
if (existsSync(networkPath))
|
|
32
|
+
return networkPath;
|
|
33
|
+
}
|
|
34
|
+
return join(profileDir, "Cookies");
|
|
35
|
+
}
|
|
36
|
+
/** Get Chrome decryption key for macOS (Keychain) */
|
|
37
|
+
function getMacKey() {
|
|
38
|
+
// Allow bypassing keychain entirely via env var (useful for SSH/CI)
|
|
39
|
+
const cachedKey = process.env.CHROME_SAFE_STORAGE_KEY;
|
|
40
|
+
if (cachedKey) {
|
|
41
|
+
return crypto.pbkdf2Sync(cachedKey, "saltysalt", 1003, 16, "sha1");
|
|
42
|
+
}
|
|
43
|
+
let chromePassword;
|
|
44
|
+
try {
|
|
45
|
+
chromePassword = execSync("security find-generic-password -s 'Chrome Safe Storage' -w", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Keychain is locked (common over SSH). Try unlocking with password from env.
|
|
49
|
+
const keychainPass = process.env.KEYCHAIN_PASSWORD;
|
|
50
|
+
if (keychainPass) {
|
|
51
|
+
try {
|
|
52
|
+
execSync(`security unlock-keychain -p "${keychainPass.replace(/"/g, '\\"')}" ~/Library/Keychains/login.keychain-db`, { stdio: "pipe" });
|
|
53
|
+
chromePassword = execSync("security find-generic-password -s 'Chrome Safe Storage' -w", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error("Cannot access macOS Keychain. The login keychain is locked.\n" +
|
|
57
|
+
"Options:\n" +
|
|
58
|
+
" 1. Run this command in a GUI terminal (not over SSH)\n" +
|
|
59
|
+
" 2. Set CHROME_SAFE_STORAGE_KEY env var (get it once: security find-generic-password -s 'Chrome Safe Storage' -w)\n" +
|
|
60
|
+
" 3. Set KEYCHAIN_PASSWORD env var to your macOS login password\n" +
|
|
61
|
+
" 4. Unlock manually: security unlock-keychain ~/Library/Keychains/login.keychain-db");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
throw new Error("Cannot access macOS Keychain. The login keychain is locked.\n" +
|
|
66
|
+
"Options:\n" +
|
|
67
|
+
" 1. Run this command in a GUI terminal (not over SSH)\n" +
|
|
68
|
+
" 2. Set CHROME_SAFE_STORAGE_KEY env var (get it once: security find-generic-password -s 'Chrome Safe Storage' -w)\n" +
|
|
69
|
+
" 3. Set KEYCHAIN_PASSWORD env var to your macOS login password\n" +
|
|
70
|
+
" 4. Unlock manually: security unlock-keychain ~/Library/Keychains/login.keychain-db");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return crypto.pbkdf2Sync(chromePassword, "saltysalt", 1003, 16, "sha1");
|
|
74
|
+
}
|
|
75
|
+
/** Get Chrome decryption key for Linux (libsecret or fallback) */
|
|
76
|
+
function getLinuxKey() {
|
|
77
|
+
let password = "peanuts"; // fallback when no keyring
|
|
78
|
+
try {
|
|
79
|
+
// Try libsecret via secret-tool
|
|
80
|
+
password = execSync("secret-tool lookup application chrome", { encoding: "utf-8" }).trim();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fall back to "peanuts" — Chrome's hardcoded fallback on Linux without a keyring
|
|
84
|
+
}
|
|
85
|
+
return crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
|
|
86
|
+
}
|
|
87
|
+
/** Get Chrome decryption key for Windows (from Local State) */
|
|
88
|
+
function getWindowsKey(profile = "Default") {
|
|
89
|
+
const home = homedir();
|
|
90
|
+
const localStatePath = join(process.env.LOCALAPPDATA || join(home, "AppData/Local"), "Google/Chrome/User Data/Local State");
|
|
91
|
+
const localState = JSON.parse(require("node:fs").readFileSync(localStatePath, "utf-8"));
|
|
92
|
+
const encryptedKey = Buffer.from(localState.os_crypt.encrypted_key, "base64");
|
|
93
|
+
// Remove "DPAPI" prefix (5 bytes)
|
|
94
|
+
const keyWithoutPrefix = encryptedKey.subarray(5);
|
|
95
|
+
// On Windows, we'd need DPAPI to decrypt — which requires native module
|
|
96
|
+
// For now, throw with guidance
|
|
97
|
+
throw new Error("Windows DPAPI decryption requires the 'win-dpapi' native module. " +
|
|
98
|
+
"Install: npm install win-dpapi");
|
|
99
|
+
}
|
|
100
|
+
/** Decrypt a single cookie value */
|
|
101
|
+
function decryptCookieValue(encryptedValue, key) {
|
|
102
|
+
if (!encryptedValue || encryptedValue.length === 0)
|
|
103
|
+
return "";
|
|
104
|
+
// v10/v11 prefix = encrypted (macOS/Linux use AES-128-CBC)
|
|
105
|
+
const prefix = encryptedValue.subarray(0, 3).toString();
|
|
106
|
+
if (prefix === "v10" || prefix === "v11") {
|
|
107
|
+
if (platform() === "win32") {
|
|
108
|
+
// Windows v10 uses AES-256-GCM
|
|
109
|
+
const nonce = encryptedValue.subarray(3, 15); // 12-byte nonce
|
|
110
|
+
const ciphertext = encryptedValue.subarray(15, -16);
|
|
111
|
+
const tag = encryptedValue.subarray(-16);
|
|
112
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
113
|
+
decipher.setAuthTag(tag);
|
|
114
|
+
return Buffer.concat([
|
|
115
|
+
decipher.update(ciphertext),
|
|
116
|
+
decipher.final(),
|
|
117
|
+
]).toString("utf-8");
|
|
118
|
+
}
|
|
119
|
+
// macOS / Linux: AES-128-CBC
|
|
120
|
+
const iv = Buffer.alloc(16, " "); // 16 spaces
|
|
121
|
+
const ciphertext = encryptedValue.subarray(3);
|
|
122
|
+
const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
|
|
123
|
+
decipher.setAutoPadding(true);
|
|
124
|
+
const decrypted = Buffer.concat([
|
|
125
|
+
decipher.update(ciphertext),
|
|
126
|
+
decipher.final(),
|
|
127
|
+
]);
|
|
128
|
+
return decrypted.toString("utf-8");
|
|
129
|
+
}
|
|
130
|
+
// Not encrypted — return as-is
|
|
131
|
+
return encryptedValue.toString("utf-8");
|
|
132
|
+
}
|
|
133
|
+
/** Map Chrome sameSite int to Playwright string */
|
|
134
|
+
function mapSameSite(value) {
|
|
135
|
+
switch (value) {
|
|
136
|
+
case 2:
|
|
137
|
+
return "Strict";
|
|
138
|
+
case 1:
|
|
139
|
+
return "Lax";
|
|
140
|
+
default:
|
|
141
|
+
return "None";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Extract cookies from local Chrome for specific domains (or all cookies if domains is empty).
|
|
146
|
+
*
|
|
147
|
+
* @param domains - Array of domains to extract (e.g., [".google.com"]). Empty = all cookies.
|
|
148
|
+
* @param profile - Chrome profile name (default: "Default")
|
|
149
|
+
*/
|
|
150
|
+
export async function extractCookies(domains, profile = "Default") {
|
|
151
|
+
const dbPath = getCookieDbPath(profile);
|
|
152
|
+
if (!existsSync(dbPath)) {
|
|
153
|
+
throw new Error(`Chrome cookie database not found at: ${dbPath}\n` +
|
|
154
|
+
`Make sure Chrome is installed and the profile "${profile}" exists.`);
|
|
155
|
+
}
|
|
156
|
+
// Copy DB to temp file to avoid lock conflicts with running Chrome
|
|
157
|
+
const tempDir = mkdtempSync(join(tmpdir(), "browser-sync-"));
|
|
158
|
+
const tempDb = join(tempDir, "Cookies");
|
|
159
|
+
copyFileSync(dbPath, tempDb);
|
|
160
|
+
// Also copy WAL/SHM if they exist (needed for recent writes)
|
|
161
|
+
for (const ext of ["-wal", "-shm"]) {
|
|
162
|
+
if (existsSync(dbPath + ext)) {
|
|
163
|
+
copyFileSync(dbPath + ext, tempDb + ext);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Get decryption key
|
|
167
|
+
let key;
|
|
168
|
+
switch (platform()) {
|
|
169
|
+
case "darwin":
|
|
170
|
+
key = getMacKey();
|
|
171
|
+
break;
|
|
172
|
+
case "linux":
|
|
173
|
+
key = getLinuxKey();
|
|
174
|
+
break;
|
|
175
|
+
case "win32":
|
|
176
|
+
key = getWindowsKey(profile);
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
throw new Error(`Unsupported platform: ${platform()}`);
|
|
180
|
+
}
|
|
181
|
+
// Query cookies — dynamic import for better-sqlite3
|
|
182
|
+
const Database = (await import("better-sqlite3")).default;
|
|
183
|
+
const db = new Database(tempDb, { readonly: true });
|
|
184
|
+
let query = `SELECT name, encrypted_value, host_key, path, expires_utc, is_httponly, is_secure, samesite FROM cookies`;
|
|
185
|
+
if (domains.length > 0) {
|
|
186
|
+
const domainClauses = domains
|
|
187
|
+
.map((d) => `host_key LIKE '%${d.replace(/'/g, "''")}'`)
|
|
188
|
+
.join(" OR ");
|
|
189
|
+
query += ` WHERE ${domainClauses}`;
|
|
190
|
+
}
|
|
191
|
+
const rows = db
|
|
192
|
+
.prepare(query)
|
|
193
|
+
.all();
|
|
194
|
+
db.close();
|
|
195
|
+
const cookies = [];
|
|
196
|
+
for (const row of rows) {
|
|
197
|
+
try {
|
|
198
|
+
const value = decryptCookieValue(row.encrypted_value, key);
|
|
199
|
+
if (!value)
|
|
200
|
+
continue;
|
|
201
|
+
cookies.push({
|
|
202
|
+
name: row.name,
|
|
203
|
+
value,
|
|
204
|
+
domain: row.host_key,
|
|
205
|
+
path: row.path,
|
|
206
|
+
// Chrome stores expires as microseconds since 1601-01-01; convert to Unix seconds
|
|
207
|
+
expires: row.expires_utc === 0
|
|
208
|
+
? -1
|
|
209
|
+
: Math.floor((row.expires_utc / 1000000) - 11644473600),
|
|
210
|
+
httpOnly: row.is_httponly === 1,
|
|
211
|
+
secure: row.is_secure === 1,
|
|
212
|
+
sameSite: mapSameSite(row.samesite),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Skip cookies that fail to decrypt (stale entries, etc.)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return cookies;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extract cookies and format as Playwright-compatible StorageState
|
|
223
|
+
*/
|
|
224
|
+
export async function extractStorageState(domains, profile = "Default") {
|
|
225
|
+
const cookies = await extractCookies(domains, profile);
|
|
226
|
+
return { cookies, origins: [] };
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEzG,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side handler for browser-sync cookie injection.
|
|
3
|
+
*
|
|
4
|
+
* Receives cookies from the CLI, connects to the tenant's Chrome via CDP,
|
|
5
|
+
* and injects them using Network.setCookies.
|
|
6
|
+
*
|
|
7
|
+
* Mount at: POST /admin/api/browser-sync/cookies
|
|
8
|
+
*/
|
|
9
|
+
import type { StorageState } from "./extract.js";
|
|
10
|
+
interface BrowserSyncRequest {
|
|
11
|
+
storageState: StorageState;
|
|
12
|
+
subdomain?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Express/Hono-style handler for the browser-sync cookie push endpoint.
|
|
16
|
+
* Integrate with the bot's admin API router.
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleBrowserSyncPush(body: BrowserSyncRequest, resolveCdpUrl: (subdomain?: string) => Promise<string>): Promise<{
|
|
19
|
+
injected: number;
|
|
20
|
+
errors: string[];
|
|
21
|
+
}>;
|
|
22
|
+
export type { BrowserSyncRequest };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side handler for browser-sync cookie injection.
|
|
3
|
+
*
|
|
4
|
+
* Receives cookies from the CLI, connects to the tenant's Chrome via CDP,
|
|
5
|
+
* and injects them using Network.setCookies.
|
|
6
|
+
*
|
|
7
|
+
* Mount at: POST /admin/api/browser-sync/cookies
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Inject cookies into a tenant's cloud Chrome via CDP.
|
|
11
|
+
*
|
|
12
|
+
* @param cdpUrl - The Chrome DevTools Protocol WebSocket URL (e.g., ws://localhost:3000)
|
|
13
|
+
* @param cookies - Playwright-format cookies to inject
|
|
14
|
+
*/
|
|
15
|
+
async function injectCookiesViaCDP(cdpUrl, cookies) {
|
|
16
|
+
// Connect to Chrome via CDP WebSocket
|
|
17
|
+
const ws = await connectCDP(cdpUrl);
|
|
18
|
+
const errors = [];
|
|
19
|
+
let injected = 0;
|
|
20
|
+
try {
|
|
21
|
+
// Convert to CDP Network.CookieParam format
|
|
22
|
+
const cdpCookies = cookies.map((c) => ({
|
|
23
|
+
name: c.name,
|
|
24
|
+
value: c.value,
|
|
25
|
+
domain: c.domain,
|
|
26
|
+
path: c.path,
|
|
27
|
+
expires: c.expires > 0 ? c.expires : undefined,
|
|
28
|
+
httpOnly: c.httpOnly,
|
|
29
|
+
secure: c.secure,
|
|
30
|
+
sameSite: c.sameSite,
|
|
31
|
+
}));
|
|
32
|
+
// Inject via Network.setCookies (batch)
|
|
33
|
+
const result = await sendCDPCommand(ws, "Network.setCookies", {
|
|
34
|
+
cookies: cdpCookies,
|
|
35
|
+
});
|
|
36
|
+
if (result.error) {
|
|
37
|
+
errors.push(result.error.message);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
injected = cdpCookies.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
ws.close();
|
|
45
|
+
}
|
|
46
|
+
return { injected, errors };
|
|
47
|
+
}
|
|
48
|
+
/** Connect to Chrome CDP endpoint */
|
|
49
|
+
async function connectCDP(cdpUrl) {
|
|
50
|
+
// Get the debugger WebSocket URL from /json/version
|
|
51
|
+
const httpUrl = cdpUrl.replace("ws://", "http://").replace("wss://", "https://");
|
|
52
|
+
const versionRes = await fetch(`${httpUrl}/json/version`);
|
|
53
|
+
const version = (await versionRes.json());
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const ws = new WebSocket(version.webSocketDebuggerUrl);
|
|
56
|
+
ws.onopen = () => resolve(ws);
|
|
57
|
+
ws.onerror = (err) => reject(new Error(`CDP connection failed: ${err}`));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Send a CDP command and wait for response */
|
|
61
|
+
function sendCDPCommand(ws, method, params) {
|
|
62
|
+
const id = Math.floor(Math.random() * 1e9);
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const handler = (event) => {
|
|
65
|
+
const data = JSON.parse(String(event.data));
|
|
66
|
+
if (data.id === id) {
|
|
67
|
+
ws.removeEventListener("message", handler);
|
|
68
|
+
resolve(data);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
ws.addEventListener("message", handler);
|
|
72
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Express/Hono-style handler for the browser-sync cookie push endpoint.
|
|
77
|
+
* Integrate with the bot's admin API router.
|
|
78
|
+
*/
|
|
79
|
+
export async function handleBrowserSyncPush(body, resolveCdpUrl) {
|
|
80
|
+
const { storageState, subdomain } = body;
|
|
81
|
+
if (!storageState?.cookies?.length) {
|
|
82
|
+
return { injected: 0, errors: ["No cookies provided"] };
|
|
83
|
+
}
|
|
84
|
+
const cdpUrl = await resolveCdpUrl(subdomain);
|
|
85
|
+
return injectCookiesViaCDP(cdpUrl, storageState.cookies);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibetechnologies/chrome-sync",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Sync browser cookies and sessions from your local Chrome to OpenClaw cloud browser",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chrome-sync": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["browser", "cookies", "sync", "chrome", "cdp", "openclaw"],
|
|
23
|
+
"author": "VibeTechnologies",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/VibeTechnologies/OpenClawBot.git",
|
|
28
|
+
"directory": "packages/browser-sync"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"better-sqlite3": "^11.0.0",
|
|
35
|
+
"commander": "^12.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "^5.5.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client for syncing cookies to OpenClaw tenant cloud browser.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. CLI starts localhost callback server, opens browser to /auth/cli
|
|
6
|
+
* 2. User authenticates on the web (Telegram OAuth or session)
|
|
7
|
+
* 3. Server redirects to localhost callback with token + username
|
|
8
|
+
* 4. CLI saves credentials, user runs `chrome-sync push`
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
12
|
+
import { createServer } from "node:http";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import type { StorageState } from "./extract.js";
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = join(homedir(), ".config", "chrome-sync");
|
|
18
|
+
const TOKEN_FILE = join(CONFIG_DIR, "auth.json");
|
|
19
|
+
|
|
20
|
+
interface AuthConfig {
|
|
21
|
+
apiUrl: string;
|
|
22
|
+
token: string;
|
|
23
|
+
username: string;
|
|
24
|
+
subdomain: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Store auth credentials */
|
|
28
|
+
export function saveAuth(config: AuthConfig): void {
|
|
29
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
30
|
+
writeFileSync(TOKEN_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Load saved auth credentials */
|
|
34
|
+
export function loadAuth(): AuthConfig | null {
|
|
35
|
+
if (!existsSync(TOKEN_FILE)) return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Clear saved auth */
|
|
44
|
+
export function clearAuth(): void {
|
|
45
|
+
if (existsSync(TOKEN_FILE)) {
|
|
46
|
+
writeFileSync(TOKEN_FILE, "", { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Login via browser OAuth callback flow.
|
|
52
|
+
*
|
|
53
|
+
* 1. Starts a localhost HTTP server on a random port
|
|
54
|
+
* 2. Opens browser to apiUrl/auth/cli?callback_port=<port>
|
|
55
|
+
* 3. User authenticates on the web
|
|
56
|
+
* 4. Server redirects to http://localhost:<port>/callback?token=...&username=...&subdomain=...
|
|
57
|
+
* 5. CLI saves credentials and closes
|
|
58
|
+
*/
|
|
59
|
+
export async function loginViaBrowser(
|
|
60
|
+
apiUrl: string = "https://console.openclaw.vibebrowser.app"
|
|
61
|
+
): Promise<AuthConfig> {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const server = createServer((req, res) => {
|
|
64
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
65
|
+
|
|
66
|
+
if (url.pathname === "/callback") {
|
|
67
|
+
const token = url.searchParams.get("token");
|
|
68
|
+
const username = url.searchParams.get("username");
|
|
69
|
+
const subdomain = url.searchParams.get("subdomain");
|
|
70
|
+
|
|
71
|
+
if (!token) {
|
|
72
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
73
|
+
res.end("<html><body><h2>Authentication failed</h2><p>No token received. Close this window and try again.</p></body></html>");
|
|
74
|
+
server.close();
|
|
75
|
+
reject(new Error("No token received from server"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const config: AuthConfig = {
|
|
80
|
+
apiUrl,
|
|
81
|
+
token,
|
|
82
|
+
username: username || "user",
|
|
83
|
+
subdomain: subdomain || "",
|
|
84
|
+
};
|
|
85
|
+
saveAuth(config);
|
|
86
|
+
|
|
87
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
88
|
+
res.end(`<html><body><h2>✓ Authenticated as ${config.username}</h2><p>You can close this window and return to the terminal.</p></body></html>`);
|
|
89
|
+
server.close();
|
|
90
|
+
resolve(config);
|
|
91
|
+
} else {
|
|
92
|
+
res.writeHead(404);
|
|
93
|
+
res.end();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
server.listen(0, "127.0.0.1", () => {
|
|
98
|
+
const addr = server.address();
|
|
99
|
+
if (!addr || typeof addr === "string") {
|
|
100
|
+
server.close();
|
|
101
|
+
reject(new Error("Failed to start callback server"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const port = addr.port;
|
|
106
|
+
const authUrl = `${apiUrl}/auth/cli?callback_port=${port}`;
|
|
107
|
+
|
|
108
|
+
// Open browser
|
|
109
|
+
import("node:child_process").then(({ exec }) => {
|
|
110
|
+
const openCmd =
|
|
111
|
+
process.platform === "darwin"
|
|
112
|
+
? `open "${authUrl}"`
|
|
113
|
+
: process.platform === "win32"
|
|
114
|
+
? `start "" "${authUrl}"`
|
|
115
|
+
: `xdg-open "${authUrl}"`;
|
|
116
|
+
exec(openCmd);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log(` Opening browser for authentication...`);
|
|
120
|
+
console.log(` If it doesn't open, visit: ${authUrl}`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Timeout after 2 minutes
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
server.close();
|
|
126
|
+
reject(new Error("Authentication timed out (2 minutes). Try again."));
|
|
127
|
+
}, 120_000);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Login with a direct token (for CI/automation).
|
|
133
|
+
*/
|
|
134
|
+
export async function loginWithToken(apiUrl: string, token: string): Promise<AuthConfig> {
|
|
135
|
+
const res = await fetch(`${apiUrl}/admin/api/users`, {
|
|
136
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new Error(`Authentication failed: ${res.status} ${res.statusText}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const config: AuthConfig = {
|
|
144
|
+
apiUrl,
|
|
145
|
+
token,
|
|
146
|
+
username: "admin",
|
|
147
|
+
subdomain: "",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
saveAuth(config);
|
|
151
|
+
return config;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Push cookies to the tenant's cloud browser.
|
|
156
|
+
*/
|
|
157
|
+
export async function pushCookies(
|
|
158
|
+
storageState: StorageState,
|
|
159
|
+
options?: { subdomain?: string }
|
|
160
|
+
): Promise<{ injected: number; errors: string[] }> {
|
|
161
|
+
const auth = loadAuth();
|
|
162
|
+
if (!auth) {
|
|
163
|
+
throw new Error("Not authenticated. Run `chrome-sync login` first.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const subdomain = options?.subdomain || auth.subdomain;
|
|
167
|
+
|
|
168
|
+
const res = await fetch(
|
|
169
|
+
`${auth.apiUrl}/admin/api/browser-sync/cookies`,
|
|
170
|
+
{
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${auth.token}`,
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
storageState,
|
|
178
|
+
subdomain: subdomain || undefined,
|
|
179
|
+
}),
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const body = await res.text();
|
|
185
|
+
throw new Error(`Failed to push cookies: ${res.status} — ${body}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return res.json() as Promise<{ injected: number; errors: string[] }>;
|
|
189
|
+
}
|