chrome-tools 1.1.10 → 1.1.11
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 +31 -0
- package/dist/src/core/db.js +102 -10
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
1
|
# chrome-tools
|
|
2
2
|
|
|
3
3
|
CLI & Node.js library for Chrome browser data, including passwords, bookmarks, history, cookies, and localStorage.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
**Node.js >= 22.16.0.** Chrome's stores are read through the built-in
|
|
8
|
+
[`node:sqlite`](https://nodejs.org/api/sqlite.html) module rather than a native
|
|
9
|
+
dependency, so there is nothing to compile at install time. 22.16.0 is the floor
|
|
10
|
+
because that is the release where the connection `timeout` option began to be
|
|
11
|
+
honoured — on 22.15 and earlier it is accepted and silently ignored, which is
|
|
12
|
+
worse than rejected.
|
|
13
|
+
|
|
14
|
+
### The experimental warning
|
|
15
|
+
|
|
16
|
+
Node prints this to **stderr** on the first `node:sqlite` load, once per process:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
(node:12345) ExperimentalWarning: SQLite is an experimental feature and might change at any time
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Measured across releases, it is emitted on **22.16 through 25.6**, and is silent
|
|
23
|
+
from **25.7.0**, where `node:sqlite` moved to Node's "release candidate"
|
|
24
|
+
stability index. So on current LTS you should expect to see it.
|
|
25
|
+
|
|
26
|
+
It is a warning, not an error: stdout stays clean, so piping JSON out of the CLI
|
|
27
|
+
is unaffected. If you drive this from a harness that treats *any* stderr output
|
|
28
|
+
as failure, set `NODE_NO_WARNINGS=1` (or run node with `--no-warnings`) in that
|
|
29
|
+
harness. This package deliberately does not suppress the warning for you — a
|
|
30
|
+
library silencing process-wide warnings would hide unrelated ones too.
|
|
31
|
+
|
|
32
|
+
Because the module is still pre-stable, its API may change between major Node
|
|
33
|
+
releases. The surface used here — `prepare`, `all`, `run`, `close`,
|
|
34
|
+
`setReadBigInts`, and `VACUUM INTO` — has been present since 22.5.0.
|
package/dist/src/core/db.js
CHANGED
|
@@ -1,13 +1,95 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
|
-
import
|
|
5
|
-
/**
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
/** SQLite result codes meaning "someone else owns this database right now". */
|
|
6
|
+
const SQLITE_BUSY = 5;
|
|
7
|
+
const SQLITE_LOCKED = 6;
|
|
8
|
+
/**
|
|
9
|
+
* Whether a failure is "another process owns this database", not a broken one.
|
|
10
|
+
*
|
|
11
|
+
* `node:sqlite` reports *every* failure as `code: "ERR_SQLITE_ERROR"` and puts
|
|
12
|
+
* the SQLite result code in `errcode`, so the `code` property is useless as a
|
|
13
|
+
* discriminator here — reading it is what an obvious port of this function from
|
|
14
|
+
* `better-sqlite3` (which set `code: "SQLITE_BUSY"`) would do, and it would call
|
|
15
|
+
* every locked store a corrupt one.
|
|
16
|
+
*
|
|
17
|
+
* Masked to the low byte because SQLite may report an *extended* code — 517
|
|
18
|
+
* (`SQLITE_BUSY_SNAPSHOT`) and 262 (`SQLITE_LOCKED_SHAREDCACHE`) are the same
|
|
19
|
+
* two conditions with detail attached, and both must take the copy path.
|
|
20
|
+
*
|
|
21
|
+
* The message check stays as the fallback: it is what caught this before
|
|
22
|
+
* `errcode` existed, and it costs nothing to keep for a build that leaves the
|
|
23
|
+
* field unset.
|
|
24
|
+
*/
|
|
6
25
|
function isLocked(err) {
|
|
7
|
-
const
|
|
26
|
+
const errcode = err?.errcode;
|
|
27
|
+
if (typeof errcode === "number") {
|
|
28
|
+
const primary = errcode & 0xff;
|
|
29
|
+
if (primary === SQLITE_BUSY || primary === SQLITE_LOCKED)
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
8
32
|
const message = err instanceof Error ? err.message : String(err);
|
|
9
|
-
return
|
|
10
|
-
|
|
33
|
+
return /database is locked|database is busy/i.test(message);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Puts a row back into the shape callers have always been handed: an ordinary
|
|
37
|
+
* object, holding plain numbers, with `Buffer` for a BLOB.
|
|
38
|
+
*
|
|
39
|
+
* `queryDatabase` is exported from the package root, so all three of these are
|
|
40
|
+
* public API rather than internal detail — a caller running its own SQL against
|
|
41
|
+
* `Login Data` gets whatever this returns.
|
|
42
|
+
*
|
|
43
|
+
* **The object itself.** `node:sqlite` builds each row with a null prototype,
|
|
44
|
+
* where better-sqlite3 returned an ordinary object. `Object.keys`, `in`, and
|
|
45
|
+
* `JSON.stringify` all still work, which is why this hides so well — but
|
|
46
|
+
* `row.hasOwnProperty(…)` is a `TypeError`, and so is anything that coerces the
|
|
47
|
+
* row to a string, including `String(row)` and dropping it in a template
|
|
48
|
+
* literal. Rebuilt here rather than re-prototyped: the loop that has to visit
|
|
49
|
+
* every cell anyway is the cheapest place to do it.
|
|
50
|
+
*
|
|
51
|
+
* **Integers.** This is the whole reason the statement is put in BigInt mode.
|
|
52
|
+
* `node:sqlite` does not quietly lose precision past `Number.MAX_SAFE_INTEGER`
|
|
53
|
+
* the way better-sqlite3 did — it **throws** `ERR_OUT_OF_RANGE` partway through
|
|
54
|
+
* reading the row. Chrome writes every timestamp as microseconds since 1601,
|
|
55
|
+
* which lands around 1.3e16 and is therefore *always* past that line, so in the
|
|
56
|
+
* default number mode a `History`, `Login Data`, or `Cookies` row cannot be read
|
|
57
|
+
* at all. Not an edge case: it is every row with a time on it. BigInt mode is
|
|
58
|
+
* the only mode that can read those columns, and this undoes the widening so
|
|
59
|
+
* nothing downstream has to learn a second numeric type. The precision that
|
|
60
|
+
* costs is precision better-sqlite3 was already dropping — sub-2-microsecond
|
|
61
|
+
* detail on a 400-year-old epoch.
|
|
62
|
+
*
|
|
63
|
+
* **Blobs.** `node:sqlite` hands back a bare `Uint8Array` where better-sqlite3
|
|
64
|
+
* gave a `Buffer`, and the difference is not a type-checking nicety: it fails
|
|
65
|
+
* *silently*. `Buffer` is a `Uint8Array`, so an `instanceof` guard still passes
|
|
66
|
+
* and nothing throws — but `blob.toString("utf-8")` on the bare array ignores
|
|
67
|
+
* the encoding and returns the decimal bytes joined by commas, so a caller
|
|
68
|
+
* decoding `password_value` or `encrypted_value` gets `"118,49,48,…"` and no
|
|
69
|
+
* error to notice. `Buffer.isBuffer` also flips to false, and the read helpers
|
|
70
|
+
* (`readUInt32BE`, `equals`, …) are simply absent.
|
|
71
|
+
*
|
|
72
|
+
* Wrapped rather than copied: each row owns its own `ArrayBuffer` here, so
|
|
73
|
+
* re-using the memory is safe and a large blob is not duplicated to change its
|
|
74
|
+
* prototype. An actual `Buffer` is left alone.
|
|
75
|
+
*/
|
|
76
|
+
function toCallerTypes(rows) {
|
|
77
|
+
return rows.map((row) => {
|
|
78
|
+
const plain = {};
|
|
79
|
+
for (const key of Object.keys(row)) {
|
|
80
|
+
const value = row[key];
|
|
81
|
+
if (typeof value === "bigint") {
|
|
82
|
+
plain[key] = Number(value);
|
|
83
|
+
}
|
|
84
|
+
else if (value instanceof Uint8Array && !Buffer.isBuffer(value)) {
|
|
85
|
+
plain[key] = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
plain[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return plain;
|
|
92
|
+
});
|
|
11
93
|
}
|
|
12
94
|
/**
|
|
13
95
|
* Copies a database SQLite will not open, alongside the sidecars that make it
|
|
@@ -44,9 +126,16 @@ export function queryDatabase(dbFilePath, sql, params = []) {
|
|
|
44
126
|
const tmpDb = join(tmpDir, basename(dbFilePath));
|
|
45
127
|
let sourceDb;
|
|
46
128
|
try {
|
|
47
|
-
sourceDb = new
|
|
48
|
-
readonly
|
|
49
|
-
|
|
129
|
+
sourceDb = new DatabaseSync(dbFilePath, {
|
|
130
|
+
// `readOnly`, not `readonly`. An unrecognized option is *ignored* rather
|
|
131
|
+
// than rejected, so the lowercase spelling — which is what better-sqlite3
|
|
132
|
+
// called it, and what this line used to say — silently hands back a
|
|
133
|
+
// writable handle onto the user's live browser profile.
|
|
134
|
+
readOnly: true,
|
|
135
|
+
// No `fileMustExist` equivalent, and none needed: a read-only open will
|
|
136
|
+
// not create the file, and the `existsSync` guard above is the explicit
|
|
137
|
+
// check that turns a missing store into a message worth reading.
|
|
138
|
+
//
|
|
50
139
|
// No wait. A running Chrome holds these stores in `locking_mode =
|
|
51
140
|
// EXCLUSIVE` for as long as it is open, so a busy timeout here buys
|
|
52
141
|
// nothing that waiting longer would win — the lock is not transient, and
|
|
@@ -90,9 +179,12 @@ export function queryDatabase(dbFilePath, sql, params = []) {
|
|
|
90
179
|
}
|
|
91
180
|
let db;
|
|
92
181
|
try {
|
|
93
|
-
db = new
|
|
182
|
+
db = new DatabaseSync(tmpDb, { readOnly: true, timeout: 5000 });
|
|
94
183
|
const stmt = db.prepare(sql);
|
|
95
|
-
|
|
184
|
+
// Read wide, hand back narrow. See `toCallerTypes` — without this, every
|
|
185
|
+
// query that touches a Chrome timestamp throws instead of returning rows.
|
|
186
|
+
stmt.setReadBigInts(true);
|
|
187
|
+
return toCallerTypes(params.length > 0 ? stmt.all(...params) : stmt.all());
|
|
96
188
|
}
|
|
97
189
|
catch (err) {
|
|
98
190
|
throw new Error(`Database query failed on '${basename(dbFilePath)}': ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-tools",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"description": "CLI & Node.js library for chrome",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -52,16 +52,14 @@
|
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"packageManager": "pnpm@11.9.0",
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"better-sqlite3": "^13.0.3",
|
|
56
55
|
"chalk": "^6.0.0",
|
|
57
56
|
"cli-table3": "^0.6.5",
|
|
58
57
|
"commander": "^15.0.0"
|
|
59
58
|
},
|
|
60
59
|
"engines": {
|
|
61
|
-
"node": ">=22.
|
|
60
|
+
"node": ">=22.16.0"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
|
-
"@types/better-sqlite3": "^9.6.0",
|
|
65
63
|
"@types/node": "^26.1.2",
|
|
66
64
|
"oxlint": "1.77.0",
|
|
67
65
|
"typescript": "^7.0.2"
|