chrome-tools 1.0.1

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.
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveTargetProfiles } from "../core/chrome.js";
4
+ import { chromeTimeToDate } from "../core/crypto.js";
5
+
6
+ /**
7
+ * Converts a Chrome bookmark timestamp (microseconds since 1601-01-01) to a date string.
8
+ * Bookmark timestamps use the same epoch as Chrome timestamps but are stored as strings.
9
+ */
10
+ function bookmarkTimeToDate(timestamp) {
11
+ if (!timestamp || timestamp === "0") return "N/A";
12
+ return chromeTimeToDate(BigInt(timestamp));
13
+ }
14
+
15
+ /**
16
+ * Recursively flattens the bookmark tree into a flat array.
17
+ *
18
+ * @param {Object} node - Bookmark tree node.
19
+ * @param {string} [folderPath=""] - Current folder path for context.
20
+ * @returns {Array<Object>}
21
+ */
22
+ function flattenBookmarks(node, folderPath = "") {
23
+ const results = [];
24
+ if (!node) return results;
25
+
26
+ const currentPath = folderPath
27
+ ? `${folderPath}/${node.name || ""}`
28
+ : node.name || "";
29
+
30
+ if (node.type === "url") {
31
+ results.push({
32
+ title: node.name || "",
33
+ url: node.url || "",
34
+ folder: folderPath,
35
+ dateAdded: bookmarkTimeToDate(node.date_added),
36
+ dateLastUsed: bookmarkTimeToDate(node.date_last_used),
37
+ id: node.id,
38
+ guid: node.guid || "",
39
+ });
40
+ }
41
+
42
+ if (node.children && Array.isArray(node.children)) {
43
+ for (const child of node.children) {
44
+ results.push(...flattenBookmarks(child, currentPath));
45
+ }
46
+ }
47
+
48
+ return results;
49
+ }
50
+
51
+ /**
52
+ * Reads and parses bookmarks from Chrome's Bookmarks JSON file.
53
+ *
54
+ * @param {Object} [options]
55
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
56
+ * @param {string} [options.profile] - Specific profile to target.
57
+ * @param {string} [options.folder] - Filter by folder name (case-insensitive).
58
+ * @param {string} [options.search] - Filter by title or URL (case-insensitive).
59
+ * @returns {Array<{profile: string, title: string, url: string, folder: string, dateAdded: string, dateLastUsed: string}>}
60
+ */
61
+ export function getBookmarks({ chromeDir, profile, folder, search } = {}) {
62
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
63
+ const results = [];
64
+
65
+ for (const p of profiles) {
66
+ const bookmarksPath = join(p.path, "Bookmarks");
67
+ if (!existsSync(bookmarksPath)) continue;
68
+
69
+ try {
70
+ const raw = readFileSync(bookmarksPath, "utf-8");
71
+ const data = JSON.parse(raw);
72
+ const roots = data.roots;
73
+
74
+ if (!roots) continue;
75
+
76
+ // Flatten all root bookmark folders
77
+ const rootNames = ["bookmark_bar", "other", "synced"];
78
+ for (const rootName of rootNames) {
79
+ if (roots[rootName]) {
80
+ const bookmarks = flattenBookmarks(roots[rootName], "");
81
+ for (const bm of bookmarks) {
82
+ bm.profile = p.name;
83
+ results.push(bm);
84
+ }
85
+ }
86
+ }
87
+ } catch {
88
+ // Skip profiles with parse errors
89
+ }
90
+ }
91
+
92
+ let filtered = results;
93
+
94
+ if (folder) {
95
+ const term = folder.toLowerCase();
96
+ filtered = filtered.filter((b) => b.folder.toLowerCase().includes(term));
97
+ }
98
+
99
+ if (search) {
100
+ const term = search.toLowerCase();
101
+ filtered = filtered.filter(
102
+ (b) =>
103
+ b.title.toLowerCase().includes(term) ||
104
+ b.url.toLowerCase().includes(term)
105
+ );
106
+ }
107
+
108
+ return filtered;
109
+ }
@@ -0,0 +1,77 @@
1
+ import { join } from "node:path";
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
+ /**
8
+ * Extracts cookies from Chrome's Cookies database.
9
+ * Encrypted values are decrypted using the same mechanism as passwords.
10
+ *
11
+ * @param {Object} [options]
12
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
13
+ * @param {string} [options.profile] - Specific profile to target.
14
+ * @param {string} [options.domain] - Filter by domain (case-insensitive, partial match).
15
+ * @param {boolean} [options.decrypt=true] - Whether to decrypt encrypted values.
16
+ * @param {number} [options.limit=100] - Maximum number of results.
17
+ * @returns {Array<Object>}
18
+ */
19
+ export function getCookies({ chromeDir, profile, domain, decrypt = true, limit = 100 } = {}) {
20
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
21
+ const derivedKey = decrypt ? deriveEncryptionKey() : null;
22
+ const results = [];
23
+
24
+ for (const p of profiles) {
25
+ const dbPath = join(p.path, "Cookies");
26
+ try {
27
+ if (!tableExists(dbPath, "cookies")) continue;
28
+
29
+ let sql = `SELECT host_key, name, value, encrypted_value, path,
30
+ expires_utc, is_secure, is_httponly, samesite,
31
+ creation_utc, last_access_utc
32
+ FROM cookies`;
33
+ const conditions = [];
34
+ const params = [];
35
+
36
+ if (domain) {
37
+ conditions.push(`host_key LIKE ?`);
38
+ params.push(`%${domain}%`);
39
+ }
40
+
41
+ if (conditions.length > 0) {
42
+ sql += ` WHERE ${conditions.join(" AND ")}`;
43
+ }
44
+ sql += ` ORDER BY host_key, name`;
45
+ if (limit) {
46
+ sql += ` LIMIT ${parseInt(limit, 10)}`;
47
+ }
48
+
49
+ const rows = queryDatabase(dbPath, sql, params);
50
+
51
+ for (const row of rows) {
52
+ let cookieValue = row.value || "";
53
+ if (!cookieValue && row.encrypted_value && derivedKey) {
54
+ cookieValue = decryptValue(row.encrypted_value, derivedKey);
55
+ }
56
+
57
+ results.push({
58
+ profile: p.name,
59
+ domain: row.host_key,
60
+ name: row.name,
61
+ value: cookieValue,
62
+ path: row.path || "/",
63
+ expires: chromeTimeToDate(row.expires_utc || 0),
64
+ secure: Boolean(row.is_secure),
65
+ httpOnly: Boolean(row.is_httponly),
66
+ sameSite: ["unspecified", "lax", "strict", "none"][row.samesite] || "unspecified",
67
+ created: chromeTimeToDate(row.creation_utc || 0),
68
+ lastAccessed: chromeTimeToDate(row.last_access_utc || 0),
69
+ });
70
+ }
71
+ } catch {
72
+ // Skip profiles with errors
73
+ }
74
+ }
75
+
76
+ return results;
77
+ }
@@ -0,0 +1,91 @@
1
+ import { join } from "node:path";
2
+ import { resolveTargetProfiles } from "../core/chrome.js";
3
+ import { chromeTimeToDate } from "../core/crypto.js";
4
+ import { queryDatabase, tableExists } from "../core/db.js";
5
+
6
+ /** Maps Chrome download state integers to human-readable strings. */
7
+ const DOWNLOAD_STATES = {
8
+ 0: "in_progress",
9
+ 1: "complete",
10
+ 2: "cancelled",
11
+ 3: "interrupted",
12
+ };
13
+
14
+ /** Maps Chrome download danger type integers to strings. */
15
+ const DANGER_TYPES = {
16
+ 0: "not_dangerous",
17
+ 1: "dangerous_file",
18
+ 2: "dangerous_url",
19
+ 3: "dangerous_content",
20
+ 4: "maybe_dangerous_content",
21
+ 5: "uncommon_content",
22
+ 6: "user_validated",
23
+ 7: "dangerous_host",
24
+ 8: "potentially_unwanted",
25
+ 9: "allowlisted_by_policy",
26
+ };
27
+
28
+ /**
29
+ * Retrieves download history from Chrome's History database.
30
+ *
31
+ * @param {Object} [options]
32
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
33
+ * @param {string} [options.profile] - Specific profile to target.
34
+ * @param {string} [options.search] - Filter by filename or URL (case-insensitive).
35
+ * @param {number} [options.limit=100] - Maximum number of results.
36
+ * @returns {Array<Object>}
37
+ */
38
+ export function getDownloads({ chromeDir, profile, search, limit = 100 } = {}) {
39
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
40
+ const results = [];
41
+
42
+ for (const p of profiles) {
43
+ const dbPath = join(p.path, "History");
44
+ try {
45
+ if (!tableExists(dbPath, "downloads")) continue;
46
+
47
+ let sql = `SELECT id, target_path, tab_url, total_bytes, received_bytes,
48
+ start_time, end_time, mime_type, original_mime_type,
49
+ state, danger_type, opened, referrer, site_url
50
+ FROM downloads ORDER BY start_time DESC`;
51
+ if (limit) {
52
+ sql += ` LIMIT ${parseInt(limit, 10)}`;
53
+ }
54
+
55
+ const rows = queryDatabase(dbPath, sql);
56
+
57
+ for (const row of rows) {
58
+ const entry = {
59
+ profile: p.name,
60
+ targetPath: row.target_path || "",
61
+ url: row.tab_url || "",
62
+ siteUrl: row.site_url || "",
63
+ referrer: row.referrer || "",
64
+ totalBytes: row.total_bytes || 0,
65
+ receivedBytes: row.received_bytes || 0,
66
+ mimeType: row.mime_type || row.original_mime_type || "",
67
+ state: DOWNLOAD_STATES[row.state] || `unknown(${row.state})`,
68
+ dangerType: DANGER_TYPES[row.danger_type] || `unknown(${row.danger_type})`,
69
+ opened: Boolean(row.opened),
70
+ startTime: chromeTimeToDate(row.start_time || 0),
71
+ endTime: chromeTimeToDate(row.end_time || 0),
72
+ };
73
+
74
+ if (search) {
75
+ const term = search.toLowerCase();
76
+ if (
77
+ !entry.targetPath.toLowerCase().includes(term) &&
78
+ !entry.url.toLowerCase().includes(term)
79
+ ) {
80
+ continue;
81
+ }
82
+ }
83
+ results.push(entry);
84
+ }
85
+ } catch {
86
+ // Skip profiles with errors
87
+ }
88
+ }
89
+
90
+ return results;
91
+ }
@@ -0,0 +1,80 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveTargetProfiles } from "../core/chrome.js";
4
+
5
+ /**
6
+ * Retrieves installed Chrome extensions from the Preferences JSON file.
7
+ *
8
+ * @param {Object} [options]
9
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
10
+ * @param {string} [options.profile] - Specific profile to target.
11
+ * @param {string} [options.search] - Filter by extension name (case-insensitive).
12
+ * @returns {Array<Object>}
13
+ */
14
+ export function getExtensions({ chromeDir, profile, search } = {}) {
15
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
16
+ const results = [];
17
+
18
+ for (const p of profiles) {
19
+ // Try Secure Preferences first, fallback to Preferences
20
+ let prefs = null;
21
+ for (const filename of ["Secure Preferences", "Preferences"]) {
22
+ const prefsPath = join(p.path, filename);
23
+ if (existsSync(prefsPath)) {
24
+ try {
25
+ prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
26
+ break;
27
+ } catch {
28
+ continue;
29
+ }
30
+ }
31
+ }
32
+
33
+ if (!prefs) continue;
34
+
35
+ const settings = prefs?.extensions?.settings;
36
+ if (!settings || typeof settings !== "object") continue;
37
+
38
+ for (const [extId, extData] of Object.entries(settings)) {
39
+ // Skip component extensions and internal Chrome extensions
40
+ if (extData.location === 5 || extData.location === 10) continue;
41
+
42
+ const manifest = extData.manifest || {};
43
+ const entry = {
44
+ profile: p.name,
45
+ id: extId,
46
+ name: manifest.name || extData.name || extId,
47
+ version: manifest.version || extData.version || "unknown",
48
+ description: manifest.description || "",
49
+ enabled: extData.state === 1,
50
+ homepageUrl: manifest.homepage_url || "",
51
+ updateUrl: manifest.update_url || "",
52
+ installTime: extData.install_time || "",
53
+ fromWebstore: extData.from_webstore || false,
54
+ location: extData.location || 0,
55
+ };
56
+
57
+ // Clean up __MSG_ placeholders in name/description
58
+ if (entry.name.startsWith("__MSG_")) {
59
+ entry.name = extId; // Fallback to ID if name is a message key
60
+ }
61
+ if (entry.description.startsWith("__MSG_")) {
62
+ entry.description = "";
63
+ }
64
+
65
+ if (search) {
66
+ const term = search.toLowerCase();
67
+ if (
68
+ !entry.name.toLowerCase().includes(term) &&
69
+ !entry.id.toLowerCase().includes(term)
70
+ ) {
71
+ continue;
72
+ }
73
+ }
74
+
75
+ results.push(entry);
76
+ }
77
+ }
78
+
79
+ return results;
80
+ }
@@ -0,0 +1,102 @@
1
+ import { join } from "node:path";
2
+ import { resolveTargetProfiles } from "../core/chrome.js";
3
+ import { chromeTimeToDate } from "../core/crypto.js";
4
+ import { queryDatabase, tableExists } from "../core/db.js";
5
+
6
+ /**
7
+ * Retrieves browsing history from Chrome's History database.
8
+ *
9
+ * @param {Object} [options]
10
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
11
+ * @param {string} [options.profile] - Specific profile to target.
12
+ * @param {string} [options.search] - Filter by URL or title (case-insensitive).
13
+ * @param {number} [options.limit=100] - Maximum number of results.
14
+ * @returns {Array<{profile: string, url: string, title: string, visitCount: number, lastVisit: string}>}
15
+ */
16
+ export function getHistory({ chromeDir, profile, search, limit = 100 } = {}) {
17
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
18
+ const results = [];
19
+
20
+ for (const p of profiles) {
21
+ const dbPath = join(p.path, "History");
22
+ try {
23
+ if (!tableExists(dbPath, "urls")) continue;
24
+
25
+ let sql = `SELECT url, title, visit_count, last_visit_time
26
+ FROM urls ORDER BY last_visit_time DESC`;
27
+ if (limit) {
28
+ sql += ` LIMIT ${parseInt(limit, 10)}`;
29
+ }
30
+
31
+ const rows = queryDatabase(dbPath, sql);
32
+
33
+ for (const row of rows) {
34
+ const entry = {
35
+ profile: p.name,
36
+ url: row.url,
37
+ title: row.title || "",
38
+ visitCount: row.visit_count || 0,
39
+ lastVisit: chromeTimeToDate(row.last_visit_time || 0),
40
+ };
41
+
42
+ if (search) {
43
+ const term = search.toLowerCase();
44
+ if (
45
+ !entry.url.toLowerCase().includes(term) &&
46
+ !entry.title.toLowerCase().includes(term)
47
+ ) {
48
+ continue;
49
+ }
50
+ }
51
+ results.push(entry);
52
+ }
53
+ } catch {
54
+ // Skip profiles with errors
55
+ }
56
+ }
57
+
58
+ return results;
59
+ }
60
+
61
+ /**
62
+ * Retrieves search terms from Chrome's History database.
63
+ *
64
+ * @param {Object} [options]
65
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
66
+ * @param {string} [options.profile] - Specific profile to target.
67
+ * @param {number} [options.limit=100] - Maximum number of results.
68
+ * @returns {Array<{profile: string, term: string, url: string}>}
69
+ */
70
+ export function getSearchTerms({ chromeDir, profile, limit = 100 } = {}) {
71
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
72
+ const results = [];
73
+
74
+ for (const p of profiles) {
75
+ const dbPath = join(p.path, "History");
76
+ try {
77
+ if (!tableExists(dbPath, "keyword_search_terms")) continue;
78
+
79
+ let sql = `SELECT kst.term, u.url
80
+ FROM keyword_search_terms kst
81
+ JOIN urls u ON kst.url_id = u.id
82
+ ORDER BY u.last_visit_time DESC`;
83
+ if (limit) {
84
+ sql += ` LIMIT ${parseInt(limit, 10)}`;
85
+ }
86
+
87
+ const rows = queryDatabase(dbPath, sql);
88
+
89
+ for (const row of rows) {
90
+ results.push({
91
+ profile: p.name,
92
+ term: row.term || "",
93
+ url: row.url || "",
94
+ });
95
+ }
96
+ } catch {
97
+ // Skip profiles with errors
98
+ }
99
+ }
100
+
101
+ return results;
102
+ }
@@ -0,0 +1,61 @@
1
+ import { join } from "node:path";
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
+ /**
8
+ * Extracts and decrypts saved passwords from Chrome's Login Data database.
9
+ *
10
+ * @param {Object} [options]
11
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
12
+ * @param {string} [options.profile] - Specific profile to target.
13
+ * @param {string} [options.search] - Filter by URL or username (case-insensitive).
14
+ * @returns {Array<{profile: string, url: string, username: string, password: string, lastUsed: string}>}
15
+ */
16
+ export function getPasswords({ chromeDir, profile, search } = {}) {
17
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
18
+ const derivedKey = deriveEncryptionKey();
19
+ const results = [];
20
+
21
+ for (const p of profiles) {
22
+ const dbPath = join(p.path, "Login Data");
23
+ try {
24
+ if (!tableExists(dbPath, "logins")) continue;
25
+ const rows = queryDatabase(
26
+ dbPath,
27
+ `SELECT origin_url, username_value, password_value, date_last_used
28
+ FROM logins ORDER BY origin_url`
29
+ );
30
+
31
+ for (const row of rows) {
32
+ const entry = {
33
+ profile: p.name,
34
+ url: row.origin_url,
35
+ username: row.username_value,
36
+ password: decryptValue(row.password_value, derivedKey),
37
+ lastUsed: chromeTimeToDate(row.date_last_used || 0),
38
+ };
39
+
40
+ if (search) {
41
+ const term = search.toLowerCase();
42
+ if (
43
+ !entry.url.toLowerCase().includes(term) &&
44
+ !entry.username.toLowerCase().includes(term)
45
+ ) {
46
+ continue;
47
+ }
48
+ }
49
+ results.push(entry);
50
+ }
51
+ } catch (err) {
52
+ // Skip profiles with errors, continue with others
53
+ results.push({
54
+ profile: p.name,
55
+ error: err.message,
56
+ });
57
+ }
58
+ }
59
+
60
+ return results.filter((r) => !r.error);
61
+ }
@@ -0,0 +1,48 @@
1
+ import { join } from "node:path";
2
+ import { resolveTargetProfiles } from "../core/chrome.js";
3
+ import { queryDatabase, tableExists } from "../core/db.js";
4
+
5
+ /**
6
+ * Retrieves search engines configured in Chrome from the Web Data database.
7
+ *
8
+ * @param {Object} [options]
9
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
10
+ * @param {string} [options.profile] - Specific profile to target.
11
+ * @returns {Array<Object>}
12
+ */
13
+ export function getSearchEngines({ chromeDir, profile } = {}) {
14
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
15
+ const results = [];
16
+
17
+ for (const p of profiles) {
18
+ const dbPath = join(p.path, "Web Data");
19
+ try {
20
+ if (!tableExists(dbPath, "keywords")) continue;
21
+
22
+ const rows = queryDatabase(
23
+ dbPath,
24
+ `SELECT id, short_name, keyword, url, suggestions_url,
25
+ favicon_url, prepopulate_id, is_active
26
+ FROM keywords ORDER BY short_name`
27
+ );
28
+
29
+ for (const row of rows) {
30
+ results.push({
31
+ profile: p.name,
32
+ id: row.id,
33
+ name: row.short_name || "",
34
+ keyword: row.keyword || "",
35
+ url: row.url || "",
36
+ suggestionsUrl: row.suggestions_url || "",
37
+ faviconUrl: row.favicon_url || "",
38
+ isDefault: row.prepopulate_id === 1,
39
+ isActive: row.is_active === 1,
40
+ });
41
+ }
42
+ } catch {
43
+ // Skip profiles with errors
44
+ }
45
+ }
46
+
47
+ return results;
48
+ }
@@ -0,0 +1,43 @@
1
+ import { join } from "node:path";
2
+ import { resolveTargetProfiles } from "../core/chrome.js";
3
+ import { queryDatabase, tableExists } from "../core/db.js";
4
+
5
+ /**
6
+ * Retrieves top sites from Chrome's Top Sites database.
7
+ *
8
+ * @param {Object} [options]
9
+ * @param {string} [options.chromeDir] - Chrome User Data directory.
10
+ * @param {string} [options.profile] - Specific profile to target.
11
+ * @returns {Array<Object>}
12
+ */
13
+ export function getTopSites({ chromeDir, profile } = {}) {
14
+ const profiles = resolveTargetProfiles({ chromeDir, profile });
15
+ const results = [];
16
+
17
+ for (const p of profiles) {
18
+ const dbPath = join(p.path, "Top Sites");
19
+ try {
20
+ if (!tableExists(dbPath, "top_sites")) continue;
21
+
22
+ const rows = queryDatabase(
23
+ dbPath,
24
+ `SELECT url, url_rank, title, redirects
25
+ FROM top_sites ORDER BY url_rank ASC`
26
+ );
27
+
28
+ for (const row of rows) {
29
+ results.push({
30
+ profile: p.name,
31
+ url: row.url || "",
32
+ title: row.title || "",
33
+ rank: row.url_rank ?? 0,
34
+ redirects: row.redirects || "",
35
+ });
36
+ }
37
+ } catch {
38
+ // Skip profiles with errors
39
+ }
40
+ }
41
+
42
+ return results;
43
+ }