chrome-tools 1.0.6 → 1.0.8
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 +1 -246
- package/dist/bin/cli.js +1 -542
- package/dist/src/core/chrome.js +1 -159
- package/dist/src/core/crypto.js +1 -69
- package/dist/src/core/db.js +1 -78
- package/dist/src/core/keychain.js +1 -53
- package/dist/src/index.js +1 -27
- package/dist/src/modules/autofill.js +1 -178
- package/dist/src/modules/bookmarks.js +1 -92
- package/dist/src/modules/cookies.js +1 -159
- package/dist/src/modules/downloads.js +1 -78
- package/dist/src/modules/extensions.js +1 -70
- package/dist/src/modules/history.js +1 -84
- package/dist/src/modules/passwords.js +1 -50
- package/dist/src/modules/search-engines.js +1 -40
- package/dist/src/modules/top-sites.js +1 -35
- package/package.json +4 -3
|
@@ -1,92 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
4
|
-
import { chromeTimeToDate } from "../core/crypto.js";
|
|
5
|
-
/**
|
|
6
|
-
* Converts a Chrome bookmark timestamp (microseconds since 1601-01-01) to a date string.
|
|
7
|
-
* Bookmark timestamps use the same epoch as Chrome timestamps but are stored as strings.
|
|
8
|
-
*/
|
|
9
|
-
function bookmarkTimeToDate(timestamp) {
|
|
10
|
-
if (!timestamp || timestamp === "0")
|
|
11
|
-
return "N/A";
|
|
12
|
-
return chromeTimeToDate(BigInt(timestamp));
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Recursively flattens the bookmark tree into a flat array.
|
|
16
|
-
*
|
|
17
|
-
* @param node - Bookmark tree node.
|
|
18
|
-
* @param folderPath - Current folder path for context.
|
|
19
|
-
* @returns Array of flattened bookmark entries.
|
|
20
|
-
*/
|
|
21
|
-
function flattenBookmarks(node, folderPath = "") {
|
|
22
|
-
const results = [];
|
|
23
|
-
if (!node)
|
|
24
|
-
return results;
|
|
25
|
-
const currentPath = folderPath
|
|
26
|
-
? `${folderPath}/${node.name || ""}`
|
|
27
|
-
: node.name || "";
|
|
28
|
-
if (node.type === "url") {
|
|
29
|
-
results.push({
|
|
30
|
-
title: node.name || "",
|
|
31
|
-
url: node.url || "",
|
|
32
|
-
folder: folderPath,
|
|
33
|
-
dateAdded: bookmarkTimeToDate(node.date_added),
|
|
34
|
-
dateLastUsed: bookmarkTimeToDate(node.date_last_used),
|
|
35
|
-
id: node.id,
|
|
36
|
-
guid: node.guid || "",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
if (node.children && Array.isArray(node.children)) {
|
|
40
|
-
for (const child of node.children) {
|
|
41
|
-
results.push(...flattenBookmarks(child, currentPath));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return results;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Reads and parses bookmarks from Chrome's Bookmarks JSON file.
|
|
48
|
-
*
|
|
49
|
-
* @param options - Options for filtering and targeting.
|
|
50
|
-
* @returns Filtered bookmark entries.
|
|
51
|
-
*/
|
|
52
|
-
export function getBookmarks({ chromeDir, profile, folder, search } = {}) {
|
|
53
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
54
|
-
const results = [];
|
|
55
|
-
for (const p of profiles) {
|
|
56
|
-
const bookmarksPath = join(p.path, "Bookmarks");
|
|
57
|
-
if (!existsSync(bookmarksPath))
|
|
58
|
-
continue;
|
|
59
|
-
try {
|
|
60
|
-
const raw = readFileSync(bookmarksPath, "utf-8");
|
|
61
|
-
const data = JSON.parse(raw);
|
|
62
|
-
const roots = data.roots;
|
|
63
|
-
if (!roots)
|
|
64
|
-
continue;
|
|
65
|
-
// Flatten all root bookmark folders
|
|
66
|
-
const rootNames = ["bookmark_bar", "other", "synced"];
|
|
67
|
-
for (const rootName of rootNames) {
|
|
68
|
-
if (roots[rootName]) {
|
|
69
|
-
const bookmarks = flattenBookmarks(roots[rootName], "");
|
|
70
|
-
for (const bm of bookmarks) {
|
|
71
|
-
bm.profile = p.name;
|
|
72
|
-
results.push(bm);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Skip profiles with parse errors
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
let filtered = results;
|
|
82
|
-
if (folder) {
|
|
83
|
-
const term = folder.toLowerCase();
|
|
84
|
-
filtered = filtered.filter((b) => b.folder.toLowerCase().includes(term));
|
|
85
|
-
}
|
|
86
|
-
if (search) {
|
|
87
|
-
const term = search.toLowerCase();
|
|
88
|
-
filtered = filtered.filter((b) => b.title.toLowerCase().includes(term) ||
|
|
89
|
-
b.url.toLowerCase().includes(term));
|
|
90
|
-
}
|
|
91
|
-
return filtered;
|
|
92
|
-
}
|
|
1
|
+
import{existsSync as o,readFileSync as r}from"node:fs";import{join as e}from"node:path";import{resolveTargetProfiles as t}from"../core/chrome.js";import{chromeTimeToDate as n}from"../core/crypto.js";function s(o){return o&&"0"!==o?n(BigInt(o)):"N/A"}function i(o,r=""){const e=[];if(!o)return e;const t=r?`${r}/${o.name||""}`:o.name||"";if("url"===o.type&&e.push({title:o.name||"",url:o.url||"",folder:r,dateAdded:s(o.date_added),dateLastUsed:s(o.date_last_used),id:o.id,guid:o.guid||""}),o.children&&Array.isArray(o.children))for(const r of o.children)e.push(...i(r,t));return e}export function getBookmarks({chromeDir:n,profile:s,folder:c,search:f}={}){const d=t({chromeDir:n,profile:s}),a=[];for(const t of d){const n=e(t.path,"Bookmarks");if(o(n))try{const o=r(n,"utf-8"),e=JSON.parse(o).roots;if(!e)continue;const s=["bookmark_bar","other","synced"];for(const o of s)if(e[o]){const r=i(e[o],"");for(const o of r)o.profile=t.name,a.push(o)}}catch{}}let l=a;if(c){const o=c.toLowerCase();l=l.filter(r=>r.folder.toLowerCase().includes(o))}if(f){const o=f.toLowerCase();l=l.filter(r=>r.title.toLowerCase().includes(o)||r.url.toLowerCase().includes(o))}return l}
|
|
@@ -1,159 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
4
|
-
import { deriveEncryptionKey } from "../core/keychain.js";
|
|
5
|
-
import { decryptValueBuffer, chromeTimeToDate } from "../core/crypto.js";
|
|
6
|
-
import { queryDatabase, tableExists } from "../core/db.js";
|
|
7
|
-
function decodeCookieValue(encryptedValue, derivedKey, hostKey) {
|
|
8
|
-
const decrypted = decryptValueBuffer(encryptedValue, derivedKey);
|
|
9
|
-
if (!Buffer.isBuffer(decrypted)) {
|
|
10
|
-
return decrypted;
|
|
11
|
-
}
|
|
12
|
-
const hostHash = createHash("sha256").update(hostKey).digest();
|
|
13
|
-
const valueBytes = decrypted.length >= hostHash.length && decrypted.subarray(0, hostHash.length).equals(hostHash)
|
|
14
|
-
? decrypted.subarray(hostHash.length)
|
|
15
|
-
: decrypted;
|
|
16
|
-
return valueBytes.toString("utf-8");
|
|
17
|
-
}
|
|
18
|
-
function parseURL(urlStr) {
|
|
19
|
-
if (!urlStr)
|
|
20
|
-
return null;
|
|
21
|
-
try {
|
|
22
|
-
return new URL(urlStr);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
try {
|
|
26
|
-
// If it doesn't have a protocol, prepend https://
|
|
27
|
-
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(urlStr)) {
|
|
28
|
-
return new URL("https://" + urlStr);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Ignore
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
function getPossibleHostKeys(hostname) {
|
|
38
|
-
const keys = new Set();
|
|
39
|
-
keys.add(hostname);
|
|
40
|
-
keys.add("." + hostname);
|
|
41
|
-
const parts = hostname.split(".");
|
|
42
|
-
for (let i = 1; i < parts.length; i++) {
|
|
43
|
-
const suffix = parts.slice(i).join(".");
|
|
44
|
-
keys.add(suffix);
|
|
45
|
-
keys.add("." + suffix);
|
|
46
|
-
}
|
|
47
|
-
return Array.from(keys);
|
|
48
|
-
}
|
|
49
|
-
function domainMatch(hostname, hostKey) {
|
|
50
|
-
if (hostname === hostKey) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
if (hostKey.startsWith(".")) {
|
|
54
|
-
if (hostname === hostKey.slice(1) || hostname.endsWith(hostKey)) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
function pathMatch(urlPath, cookiePath) {
|
|
61
|
-
const uPath = urlPath || "/";
|
|
62
|
-
const cPath = cookiePath || "/";
|
|
63
|
-
if (cPath === "/") {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
if (uPath === cPath) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
if (uPath.startsWith(cPath)) {
|
|
70
|
-
if (cPath.endsWith("/") || uPath[cPath.length] === "/") {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Extracts cookies from Chrome's Cookies database.
|
|
78
|
-
* Encrypted values are decrypted using the same mechanism as passwords.
|
|
79
|
-
*
|
|
80
|
-
* @param options - Targeting and filtering options.
|
|
81
|
-
* @returns Array of cookie entries.
|
|
82
|
-
*/
|
|
83
|
-
export function getCookies({ chromeDir, profile, domain, url, decrypt = true, limit = 100, } = {}) {
|
|
84
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
85
|
-
const derivedKey = decrypt ? deriveEncryptionKey() : null;
|
|
86
|
-
const results = [];
|
|
87
|
-
const parsedUrl = parseURL(url);
|
|
88
|
-
for (const p of profiles) {
|
|
89
|
-
const dbPath = join(p.path, "Cookies");
|
|
90
|
-
try {
|
|
91
|
-
if (!tableExists(dbPath, "cookies"))
|
|
92
|
-
continue;
|
|
93
|
-
let sql = `SELECT host_key, name, value, encrypted_value, path,
|
|
94
|
-
expires_utc, is_secure, is_httponly, samesite,
|
|
95
|
-
creation_utc, last_access_utc
|
|
96
|
-
FROM cookies`;
|
|
97
|
-
const conditions = [];
|
|
98
|
-
const params = [];
|
|
99
|
-
if (domain) {
|
|
100
|
-
conditions.push(`host_key LIKE ?`);
|
|
101
|
-
params.push(`%${domain}%`);
|
|
102
|
-
}
|
|
103
|
-
else if (parsedUrl) {
|
|
104
|
-
const hostKeys = getPossibleHostKeys(parsedUrl.hostname);
|
|
105
|
-
const placeholders = hostKeys.map(() => "?").join(", ");
|
|
106
|
-
conditions.push(`host_key IN (${placeholders})`);
|
|
107
|
-
params.push(...hostKeys);
|
|
108
|
-
}
|
|
109
|
-
if (conditions.length > 0) {
|
|
110
|
-
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
111
|
-
}
|
|
112
|
-
sql += ` ORDER BY host_key, name`;
|
|
113
|
-
if (limit && !url) {
|
|
114
|
-
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
115
|
-
}
|
|
116
|
-
const rows = queryDatabase(dbPath, sql, params);
|
|
117
|
-
for (const row of rows) {
|
|
118
|
-
if (parsedUrl) {
|
|
119
|
-
if (!domainMatch(parsedUrl.hostname, row.host_key || ""))
|
|
120
|
-
continue;
|
|
121
|
-
if (!pathMatch(parsedUrl.pathname || "/", row.path || "/"))
|
|
122
|
-
continue;
|
|
123
|
-
if (row.is_secure && parsedUrl.protocol !== "https:")
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
let cookieValue = row.value || "";
|
|
127
|
-
if (!cookieValue && row.encrypted_value && derivedKey) {
|
|
128
|
-
cookieValue = decodeCookieValue(row.encrypted_value, derivedKey, row.host_key || "");
|
|
129
|
-
}
|
|
130
|
-
results.push({
|
|
131
|
-
profile: p.name,
|
|
132
|
-
domain: row.host_key || "",
|
|
133
|
-
name: row.name || "",
|
|
134
|
-
value: cookieValue,
|
|
135
|
-
path: row.path || "/",
|
|
136
|
-
expires: chromeTimeToDate(row.expires_utc || 0),
|
|
137
|
-
secure: Boolean(row.is_secure),
|
|
138
|
-
httpOnly: Boolean(row.is_httponly),
|
|
139
|
-
sameSite: ["unspecified", "lax", "strict", "none"][row.samesite] || "unspecified",
|
|
140
|
-
created: chromeTimeToDate(row.creation_utc || 0),
|
|
141
|
-
lastAccessed: chromeTimeToDate(row.last_access_utc || 0),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Skip profiles with errors
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (limit && url) {
|
|
150
|
-
return results.slice(0, parseInt(limit, 10));
|
|
151
|
-
}
|
|
152
|
-
return results;
|
|
153
|
-
}
|
|
154
|
-
export const _test = {
|
|
155
|
-
parseURL,
|
|
156
|
-
getPossibleHostKeys,
|
|
157
|
-
domainMatch,
|
|
158
|
-
pathMatch,
|
|
159
|
-
};
|
|
1
|
+
import{join as t}from"node:path";import{createHash as e}from"node:crypto";import{resolveTargetProfiles as n}from"../core/chrome.js";import{deriveEncryptionKey as o}from"../core/keychain.js";import{decryptValueBuffer as s,chromeTimeToDate as r}from"../core/crypto.js";import{queryDatabase as c,tableExists as i}from"../core/db.js";function a(t,n,o){const r=s(t,n);if(!Buffer.isBuffer(r))return r;const c=e("sha256").update(o).digest();return(r.length>=c.length&&r.subarray(0,c.length).equals(c)?r.subarray(c.length):r).toString("utf-8")}function u(t){if(!t)return null;try{return new URL(t)}catch{try{if(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(t))return new URL("https://"+t)}catch{}}return null}function h(t){const e=new Set;e.add(t),e.add("."+t);const n=t.split(".");for(let t=1;t<n.length;t++){const o=n.slice(t).join(".");e.add(o),e.add("."+o)}return Array.from(e)}function p(t,e){return t===e||!(!e.startsWith(".")||t!==e.slice(1)&&!t.endsWith(e))}function l(t,e){const n=t||"/",o=e||"/";return"/"===o||(n===o||!(!n.startsWith(o)||!o.endsWith("/")&&"/"!==n[o.length]))}export function getCookies({chromeDir:e,profile:s,domain:f,url:m,decrypt:d=!0,limit:y=100}={}){const _=n({chromeDir:e,profile:s}),k=d?o():null,g=[],j=u(m);for(const e of _){const n=t(e.path,"Cookies");try{if(!i(n,"cookies"))continue;let t="SELECT host_key, name, value, encrypted_value, path,\n expires_utc, is_secure, is_httponly, samesite,\n creation_utc, last_access_utc\n FROM cookies";const o=[],s=[];if(f)o.push("host_key LIKE ?"),s.push(`%${f}%`);else if(j){const t=h(j.hostname),e=t.map(()=>"?").join(", ");o.push(`host_key IN (${e})`),s.push(...t)}o.length>0&&(t+=` WHERE ${o.join(" AND ")}`),t+=" ORDER BY host_key, name",y&&!m&&(t+=` LIMIT ${parseInt(y,10)}`);const u=c(n,t,s);for(const t of u){if(j){if(!p(j.hostname,t.host_key||""))continue;if(!l(j.pathname||"/",t.path||"/"))continue;if(t.is_secure&&"https:"!==j.protocol)continue}let n=t.value||"";!n&&t.encrypted_value&&k&&(n=a(t.encrypted_value,k,t.host_key||"")),g.push({profile:e.name,domain:t.host_key||"",name:t.name||"",value:n,path:t.path||"/",expires:r(t.expires_utc||0),secure:Boolean(t.is_secure),httpOnly:Boolean(t.is_httponly),sameSite:["unspecified","lax","strict","none"][t.samesite]||"unspecified",created:r(t.creation_utc||0),lastAccessed:r(t.last_access_utc||0)})}}catch{}}return y&&m?g.slice(0,parseInt(y,10)):g}export const _test={parseURL:u,getPossibleHostKeys:h,domainMatch:p,pathMatch:l};
|
|
@@ -1,78 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { chromeTimeToDate } from "../core/crypto.js";
|
|
4
|
-
import { queryDatabase, tableExists } from "../core/db.js";
|
|
5
|
-
/** Maps Chrome download state integers to human-readable strings. */
|
|
6
|
-
const DOWNLOAD_STATES = {
|
|
7
|
-
0: "in_progress",
|
|
8
|
-
1: "complete",
|
|
9
|
-
2: "cancelled",
|
|
10
|
-
3: "interrupted",
|
|
11
|
-
};
|
|
12
|
-
/** Maps Chrome download danger type integers to strings. */
|
|
13
|
-
const DANGER_TYPES = {
|
|
14
|
-
0: "not_dangerous",
|
|
15
|
-
1: "dangerous_file",
|
|
16
|
-
2: "dangerous_url",
|
|
17
|
-
3: "dangerous_content",
|
|
18
|
-
4: "maybe_dangerous_content",
|
|
19
|
-
5: "uncommon_content",
|
|
20
|
-
6: "user_validated",
|
|
21
|
-
7: "dangerous_host",
|
|
22
|
-
8: "potentially_unwanted",
|
|
23
|
-
9: "allowlisted_by_policy",
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Retrieves download history from Chrome's History database.
|
|
27
|
-
*
|
|
28
|
-
* @param options - Targeting, filtering, and limit options.
|
|
29
|
-
* @returns Array of download entries.
|
|
30
|
-
*/
|
|
31
|
-
export function getDownloads({ chromeDir, profile, search, limit = 100, } = {}) {
|
|
32
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
33
|
-
const results = [];
|
|
34
|
-
for (const p of profiles) {
|
|
35
|
-
const dbPath = join(p.path, "History");
|
|
36
|
-
try {
|
|
37
|
-
if (!tableExists(dbPath, "downloads"))
|
|
38
|
-
continue;
|
|
39
|
-
let sql = `SELECT id, target_path, tab_url, total_bytes, received_bytes,
|
|
40
|
-
start_time, end_time, mime_type, original_mime_type,
|
|
41
|
-
state, danger_type, opened, referrer, site_url
|
|
42
|
-
FROM downloads ORDER BY start_time DESC`;
|
|
43
|
-
if (limit) {
|
|
44
|
-
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
45
|
-
}
|
|
46
|
-
const rows = queryDatabase(dbPath, sql);
|
|
47
|
-
for (const row of rows) {
|
|
48
|
-
const entry = {
|
|
49
|
-
profile: p.name,
|
|
50
|
-
targetPath: row.target_path || "",
|
|
51
|
-
url: row.tab_url || "",
|
|
52
|
-
siteUrl: row.site_url || "",
|
|
53
|
-
referrer: row.referrer || "",
|
|
54
|
-
totalBytes: row.total_bytes || 0,
|
|
55
|
-
receivedBytes: row.received_bytes || 0,
|
|
56
|
-
mimeType: row.mime_type || row.original_mime_type || "",
|
|
57
|
-
state: DOWNLOAD_STATES[row.state] || `unknown(${row.state})`,
|
|
58
|
-
dangerType: DANGER_TYPES[row.danger_type] || `unknown(${row.danger_type})`,
|
|
59
|
-
opened: Boolean(row.opened),
|
|
60
|
-
startTime: chromeTimeToDate(row.start_time || 0),
|
|
61
|
-
endTime: chromeTimeToDate(row.end_time || 0),
|
|
62
|
-
};
|
|
63
|
-
if (search) {
|
|
64
|
-
const term = search.toLowerCase();
|
|
65
|
-
if (!(entry.targetPath && entry.targetPath.toLowerCase().includes(term)) &&
|
|
66
|
-
!(entry.url && entry.url.toLowerCase().includes(term))) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
results.push(entry);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Skip profiles with errors
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return results;
|
|
78
|
-
}
|
|
1
|
+
import{join as e}from"node:path";import{resolveTargetProfiles as t}from"../core/chrome.js";import{chromeTimeToDate as r}from"../core/crypto.js";import{queryDatabase as o,tableExists as n}from"../core/db.js";const a={0:"in_progress",1:"complete",2:"cancelled",3:"interrupted"},s={0:"not_dangerous",1:"dangerous_file",2:"dangerous_url",3:"dangerous_content",4:"maybe_dangerous_content",5:"uncommon_content",6:"user_validated",7:"dangerous_host",8:"potentially_unwanted",9:"allowlisted_by_policy"};export function getDownloads({chromeDir:i,profile:_,search:l,limit:d=100}={}){const c=t({chromeDir:i,profile:_}),m=[];for(const t of c){const i=e(t.path,"History");try{if(!n(i,"downloads"))continue;let e="SELECT id, target_path, tab_url, total_bytes, received_bytes,\n start_time, end_time, mime_type, original_mime_type,\n state, danger_type, opened, referrer, site_url\n FROM downloads ORDER BY start_time DESC";d&&(e+=` LIMIT ${parseInt(d,10)}`);const _=o(i,e);for(const e of _){const o={profile:t.name,targetPath:e.target_path||"",url:e.tab_url||"",siteUrl:e.site_url||"",referrer:e.referrer||"",totalBytes:e.total_bytes||0,receivedBytes:e.received_bytes||0,mimeType:e.mime_type||e.original_mime_type||"",state:a[e.state]||`unknown(${e.state})`,dangerType:s[e.danger_type]||`unknown(${e.danger_type})`,opened:Boolean(e.opened),startTime:r(e.start_time||0),endTime:r(e.end_time||0)};if(l){const e=l.toLowerCase();if(!(o.targetPath&&o.targetPath.toLowerCase().includes(e)||o.url&&o.url.toLowerCase().includes(e)))continue}m.push(o)}}catch{}}return m}
|
|
@@ -1,70 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
4
|
-
/**
|
|
5
|
-
* Retrieves installed Chrome extensions from the Preferences JSON file.
|
|
6
|
-
*
|
|
7
|
-
* @param options - Targeting and filtering options.
|
|
8
|
-
* @returns Array of extension entries.
|
|
9
|
-
*/
|
|
10
|
-
export function getExtensions({ chromeDir, profile, search } = {}) {
|
|
11
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
12
|
-
const results = [];
|
|
13
|
-
for (const p of profiles) {
|
|
14
|
-
// Try Secure Preferences first, fallback to Preferences
|
|
15
|
-
let prefs = null;
|
|
16
|
-
for (const filename of ["Secure Preferences", "Preferences"]) {
|
|
17
|
-
const prefsPath = join(p.path, filename);
|
|
18
|
-
if (existsSync(prefsPath)) {
|
|
19
|
-
try {
|
|
20
|
-
prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (!prefs)
|
|
29
|
-
continue;
|
|
30
|
-
const settings = prefs?.extensions?.settings;
|
|
31
|
-
if (!settings || typeof settings !== "object")
|
|
32
|
-
continue;
|
|
33
|
-
for (const [extId, extDataAny] of Object.entries(settings)) {
|
|
34
|
-
const extData = extDataAny;
|
|
35
|
-
// Skip component extensions and internal Chrome extensions
|
|
36
|
-
if (extData.location === 5 || extData.location === 10)
|
|
37
|
-
continue;
|
|
38
|
-
const manifest = extData.manifest || {};
|
|
39
|
-
const entry = {
|
|
40
|
-
profile: p.name,
|
|
41
|
-
id: extId,
|
|
42
|
-
name: manifest.name || extData.name || extId,
|
|
43
|
-
version: manifest.version || extData.version || "unknown",
|
|
44
|
-
description: manifest.description || "",
|
|
45
|
-
enabled: extData.state === 1,
|
|
46
|
-
homepageUrl: manifest.homepage_url || "",
|
|
47
|
-
updateUrl: manifest.update_url || "",
|
|
48
|
-
installTime: extData.install_time || "",
|
|
49
|
-
fromWebstore: extData.from_webstore || false,
|
|
50
|
-
location: extData.location || 0,
|
|
51
|
-
};
|
|
52
|
-
// Clean up __MSG_ placeholders in name/description
|
|
53
|
-
if (entry.name.startsWith("__MSG_")) {
|
|
54
|
-
entry.name = extId; // Fallback to ID if name is a message key
|
|
55
|
-
}
|
|
56
|
-
if (entry.description.startsWith("__MSG_")) {
|
|
57
|
-
entry.description = "";
|
|
58
|
-
}
|
|
59
|
-
if (search) {
|
|
60
|
-
const term = search.toLowerCase();
|
|
61
|
-
if (!(entry.name && entry.name.toLowerCase().includes(term)) &&
|
|
62
|
-
!(entry.id && entry.id.toLowerCase().includes(term))) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
results.push(entry);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return results;
|
|
70
|
-
}
|
|
1
|
+
import{existsSync as e,readFileSync as o}from"node:fs";import{join as t}from"node:path";import{resolveTargetProfiles as n}from"../core/chrome.js";export function getExtensions({chromeDir:r,profile:i,search:s}={}){const c=n({chromeDir:r,profile:i}),a=[];for(const n of c){let r=null;for(const i of["Secure Preferences","Preferences"]){const s=t(n.path,i);if(e(s))try{r=JSON.parse(o(s,"utf-8"));break}catch{continue}}if(!r)continue;const i=r?.extensions?.settings;if(i&&"object"==typeof i)for(const[e,o]of Object.entries(i)){const t=o;if(5===t.location||10===t.location)continue;const r=t.manifest||{},i={profile:n.name,id:e,name:r.name||t.name||e,version:r.version||t.version||"unknown",description:r.description||"",enabled:1===t.state,homepageUrl:r.homepage_url||"",updateUrl:r.update_url||"",installTime:t.install_time||"",fromWebstore:t.from_webstore||!1,location:t.location||0};if(i.name.startsWith("__MSG_")&&(i.name=e),i.description.startsWith("__MSG_")&&(i.description=""),s){const e=s.toLowerCase();if(!(i.name&&i.name.toLowerCase().includes(e)||i.id&&i.id.toLowerCase().includes(e)))continue}a.push(i)}}return a}
|
|
@@ -1,84 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { chromeTimeToDate } from "../core/crypto.js";
|
|
4
|
-
import { queryDatabase, tableExists } from "../core/db.js";
|
|
5
|
-
/**
|
|
6
|
-
* Retrieves browsing history from Chrome's History database.
|
|
7
|
-
*
|
|
8
|
-
* @param options - Filtering and targeting options.
|
|
9
|
-
* @returns Array of history entries.
|
|
10
|
-
*/
|
|
11
|
-
export function getHistory({ chromeDir, profile, search, limit = 100 } = {}) {
|
|
12
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
13
|
-
const results = [];
|
|
14
|
-
for (const p of profiles) {
|
|
15
|
-
const dbPath = join(p.path, "History");
|
|
16
|
-
try {
|
|
17
|
-
if (!tableExists(dbPath, "urls"))
|
|
18
|
-
continue;
|
|
19
|
-
let sql = `SELECT url, title, visit_count, last_visit_time
|
|
20
|
-
FROM urls ORDER BY last_visit_time DESC`;
|
|
21
|
-
if (limit) {
|
|
22
|
-
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
23
|
-
}
|
|
24
|
-
const rows = queryDatabase(dbPath, sql);
|
|
25
|
-
for (const row of rows) {
|
|
26
|
-
const entry = {
|
|
27
|
-
profile: p.name,
|
|
28
|
-
url: row.url,
|
|
29
|
-
title: row.title || "",
|
|
30
|
-
visitCount: row.visit_count || 0,
|
|
31
|
-
lastVisit: chromeTimeToDate(row.last_visit_time || 0),
|
|
32
|
-
};
|
|
33
|
-
if (search) {
|
|
34
|
-
const term = search.toLowerCase();
|
|
35
|
-
if (!(entry.url && entry.url.toLowerCase().includes(term)) &&
|
|
36
|
-
!(entry.title && entry.title.toLowerCase().includes(term))) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
results.push(entry);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Skip profiles with errors
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return results;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Retrieves search terms from Chrome's History database.
|
|
51
|
-
*
|
|
52
|
-
* @param options - Targeting and limit options.
|
|
53
|
-
* @returns Array of search term entries.
|
|
54
|
-
*/
|
|
55
|
-
export function getSearchTerms({ chromeDir, profile, limit = 100 } = {}) {
|
|
56
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
57
|
-
const results = [];
|
|
58
|
-
for (const p of profiles) {
|
|
59
|
-
const dbPath = join(p.path, "History");
|
|
60
|
-
try {
|
|
61
|
-
if (!tableExists(dbPath, "keyword_search_terms"))
|
|
62
|
-
continue;
|
|
63
|
-
let sql = `SELECT kst.term, u.url
|
|
64
|
-
FROM keyword_search_terms kst
|
|
65
|
-
JOIN urls u ON kst.url_id = u.id
|
|
66
|
-
ORDER BY u.last_visit_time DESC`;
|
|
67
|
-
if (limit) {
|
|
68
|
-
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
69
|
-
}
|
|
70
|
-
const rows = queryDatabase(dbPath, sql);
|
|
71
|
-
for (const row of rows) {
|
|
72
|
-
results.push({
|
|
73
|
-
profile: p.name,
|
|
74
|
-
term: row.term || "",
|
|
75
|
-
url: row.url || "",
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// Skip profiles with errors
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return results;
|
|
84
|
-
}
|
|
1
|
+
import{join as t}from"node:path";import{resolveTargetProfiles as r}from"../core/chrome.js";import{chromeTimeToDate as o}from"../core/crypto.js";import{queryDatabase as e,tableExists as i}from"../core/db.js";export function getHistory({chromeDir:s,profile:c,search:n,limit:l=100}={}){const u=r({chromeDir:s,profile:c}),m=[];for(const r of u){const s=t(r.path,"History");try{if(!i(s,"urls"))continue;let t="SELECT url, title, visit_count, last_visit_time\n FROM urls ORDER BY last_visit_time DESC";l&&(t+=` LIMIT ${parseInt(l,10)}`);const c=e(s,t);for(const t of c){const e={profile:r.name,url:t.url,title:t.title||"",visitCount:t.visit_count||0,lastVisit:o(t.last_visit_time||0)};if(n){const t=n.toLowerCase();if(!(e.url&&e.url.toLowerCase().includes(t)||e.title&&e.title.toLowerCase().includes(t)))continue}m.push(e)}}catch{}}return m}export function getSearchTerms({chromeDir:o,profile:s,limit:c=100}={}){const n=r({chromeDir:o,profile:s}),l=[];for(const r of n){const o=t(r.path,"History");try{if(!i(o,"keyword_search_terms"))continue;let t="SELECT kst.term, u.url\n FROM keyword_search_terms kst\n JOIN urls u ON kst.url_id = u.id\n ORDER BY u.last_visit_time DESC";c&&(t+=` LIMIT ${parseInt(c,10)}`);const s=e(o,t);for(const t of s)l.push({profile:r.name,term:t.term||"",url:t.url||""})}catch{}}return l}
|
|
@@ -1,50 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { deriveEncryptionKey } from "../core/keychain.js";
|
|
4
|
-
import { decryptValue, chromeTimeToDate } from "../core/crypto.js";
|
|
5
|
-
import { queryDatabase, tableExists } from "../core/db.js";
|
|
6
|
-
/**
|
|
7
|
-
* Extracts and decrypts saved passwords from Chrome's Login Data database.
|
|
8
|
-
*
|
|
9
|
-
* @param options - Options for profile selection and filtering.
|
|
10
|
-
* @returns Array of decrypted password entries.
|
|
11
|
-
*/
|
|
12
|
-
export function getPasswords({ chromeDir, profile, search } = {}) {
|
|
13
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
14
|
-
const derivedKey = deriveEncryptionKey();
|
|
15
|
-
const results = [];
|
|
16
|
-
for (const p of profiles) {
|
|
17
|
-
const dbPath = join(p.path, "Login Data");
|
|
18
|
-
try {
|
|
19
|
-
if (!tableExists(dbPath, "logins"))
|
|
20
|
-
continue;
|
|
21
|
-
const rows = queryDatabase(dbPath, `SELECT origin_url, username_value, password_value, date_last_used
|
|
22
|
-
FROM logins ORDER BY origin_url`);
|
|
23
|
-
for (const row of rows) {
|
|
24
|
-
const entry = {
|
|
25
|
-
profile: p.name,
|
|
26
|
-
url: row.origin_url,
|
|
27
|
-
username: row.username_value,
|
|
28
|
-
password: decryptValue(row.password_value, derivedKey),
|
|
29
|
-
lastUsed: chromeTimeToDate(row.date_last_used || 0),
|
|
30
|
-
};
|
|
31
|
-
if (search) {
|
|
32
|
-
const term = search.toLowerCase();
|
|
33
|
-
if (!(entry.url && entry.url.toLowerCase().includes(term)) &&
|
|
34
|
-
!(entry.username && entry.username.toLowerCase().includes(term))) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
results.push(entry);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
// Skip profiles with errors, continue with others
|
|
43
|
-
results.push({
|
|
44
|
-
profile: p.name,
|
|
45
|
-
error: err.message,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return results.filter((r) => !r.error);
|
|
50
|
-
}
|
|
1
|
+
import{join as r}from"node:path";import{resolveTargetProfiles as o}from"../core/chrome.js";import{deriveEncryptionKey as e}from"../core/keychain.js";import{decryptValue as s,chromeTimeToDate as t}from"../core/crypto.js";import{queryDatabase as n,tableExists as a}from"../core/db.js";export function getPasswords({chromeDir:i,profile:u,search:c}={}){const l=o({chromeDir:i,profile:u}),m=e(),f=[];for(const o of l){const e=r(o.path,"Login Data");try{if(!a(e,"logins"))continue;const r=n(e,"SELECT origin_url, username_value, password_value, date_last_used\n FROM logins ORDER BY origin_url");for(const e of r){const r={profile:o.name,url:e.origin_url,username:e.username_value,password:s(e.password_value,m),lastUsed:t(e.date_last_used||0)};if(c){const o=c.toLowerCase();if(!(r.url&&r.url.toLowerCase().includes(o)||r.username&&r.username.toLowerCase().includes(o)))continue}f.push(r)}}catch(r){f.push({profile:o.name,error:r.message})}}return f.filter(r=>!r.error)}
|
|
@@ -1,40 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { queryDatabase, tableExists } from "../core/db.js";
|
|
4
|
-
/**
|
|
5
|
-
* Retrieves search engines configured in Chrome from the Web Data database.
|
|
6
|
-
*
|
|
7
|
-
* @param options - Targeting options.
|
|
8
|
-
* @returns Array of search engine entries.
|
|
9
|
-
*/
|
|
10
|
-
export function getSearchEngines({ chromeDir, profile } = {}) {
|
|
11
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
12
|
-
const results = [];
|
|
13
|
-
for (const p of profiles) {
|
|
14
|
-
const dbPath = join(p.path, "Web Data");
|
|
15
|
-
try {
|
|
16
|
-
if (!tableExists(dbPath, "keywords"))
|
|
17
|
-
continue;
|
|
18
|
-
const rows = queryDatabase(dbPath, `SELECT id, short_name, keyword, url, suggestions_url,
|
|
19
|
-
favicon_url, prepopulate_id, is_active
|
|
20
|
-
FROM keywords ORDER BY short_name`);
|
|
21
|
-
for (const row of rows) {
|
|
22
|
-
results.push({
|
|
23
|
-
profile: p.name,
|
|
24
|
-
id: row.id,
|
|
25
|
-
name: row.short_name || "",
|
|
26
|
-
keyword: row.keyword || "",
|
|
27
|
-
url: row.url || "",
|
|
28
|
-
suggestionsUrl: row.suggestions_url || "",
|
|
29
|
-
faviconUrl: row.favicon_url || "",
|
|
30
|
-
isDefault: row.prepopulate_id === 1,
|
|
31
|
-
isActive: row.is_active === 1,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// Skip profiles with errors
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return results;
|
|
40
|
-
}
|
|
1
|
+
import{join as o}from"node:path";import{resolveTargetProfiles as r}from"../core/chrome.js";import{queryDatabase as e,tableExists as i}from"../core/db.js";export function getSearchEngines({chromeDir:t,profile:s}={}){const n=r({chromeDir:t,profile:s}),c=[];for(const r of n){const t=o(r.path,"Web Data");try{if(!i(t,"keywords"))continue;const o=e(t,"SELECT id, short_name, keyword, url, suggestions_url,\n favicon_url, prepopulate_id, is_active\n FROM keywords ORDER BY short_name");for(const e of o)c.push({profile:r.name,id:e.id,name:e.short_name||"",keyword:e.keyword||"",url:e.url||"",suggestionsUrl:e.suggestions_url||"",faviconUrl:e.favicon_url||"",isDefault:1===e.prepopulate_id,isActive:1===e.is_active})}catch{}}return c}
|
|
@@ -1,35 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { queryDatabase, tableExists } from "../core/db.js";
|
|
4
|
-
/**
|
|
5
|
-
* Retrieves top sites from Chrome's Top Sites database.
|
|
6
|
-
*
|
|
7
|
-
* @param options - Targeting options.
|
|
8
|
-
* @returns Array of top site entries.
|
|
9
|
-
*/
|
|
10
|
-
export function getTopSites({ chromeDir, profile } = {}) {
|
|
11
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
12
|
-
const results = [];
|
|
13
|
-
for (const p of profiles) {
|
|
14
|
-
const dbPath = join(p.path, "Top Sites");
|
|
15
|
-
try {
|
|
16
|
-
if (!tableExists(dbPath, "top_sites"))
|
|
17
|
-
continue;
|
|
18
|
-
const rows = queryDatabase(dbPath, `SELECT url, url_rank, title, redirects
|
|
19
|
-
FROM top_sites ORDER BY url_rank ASC`);
|
|
20
|
-
for (const row of rows) {
|
|
21
|
-
results.push({
|
|
22
|
-
profile: p.name,
|
|
23
|
-
url: row.url || "",
|
|
24
|
-
title: row.title || "",
|
|
25
|
-
rank: row.url_rank ?? 0,
|
|
26
|
-
redirects: row.redirects || "",
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
// Skip profiles with errors
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return results;
|
|
35
|
-
}
|
|
1
|
+
import{join as r}from"node:path";import{resolveTargetProfiles as t}from"../core/chrome.js";import{queryDatabase as o,tableExists as e}from"../core/db.js";export function getTopSites({chromeDir:i,profile:c}={}){const n=t({chromeDir:i,profile:c}),s=[];for(const t of n){const i=r(t.path,"Top Sites");try{if(!e(i,"top_sites"))continue;const r=o(i,"SELECT url, url_rank, title, redirects\n FROM top_sites ORDER BY url_rank ASC");for(const o of r)s.push({profile:t.name,url:o.url||"",title:o.title||"",rank:o.url_rank??0,redirects:o.redirects||""})}catch{}}return s}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-tools",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI & Node.js library
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "CLI & Node.js library for chrome",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc",
|
|
29
|
+
"build": "node scripts/clean.mjs && tsc && node scripts/minify.mjs",
|
|
30
30
|
"start": "node dist/bin/cli.js",
|
|
31
31
|
"test": "npm run build && node --test dist/test/*.test.js dist/test/**/*.test.js && node dist/bin/cli.js --help",
|
|
32
32
|
"release": "npm run build && npm version patch --no-git-tag-version && npm publish"
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/better-sqlite3": "^7.6.13",
|
|
58
58
|
"@types/node": "^25.9.1",
|
|
59
|
+
"terser": "^5.44.0",
|
|
59
60
|
"typescript": "^6.0.3"
|
|
60
61
|
}
|
|
61
62
|
}
|