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 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.
@@ -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 Database from "better-sqlite3";
5
- /** Whether a failure is "another process owns this database", not a broken one. */
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 code = err?.code;
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 ((typeof code === "string" && code.startsWith("SQLITE_BUSY")) ||
10
- /database is locked|database is busy/i.test(message));
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 Database(dbFilePath, {
48
- readonly: true,
49
- fileMustExist: true,
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 Database(tmpDb, { readonly: true, timeout: 5000 });
182
+ db = new DatabaseSync(tmpDb, { readOnly: true, timeout: 5000 });
94
183
  const stmt = db.prepare(sql);
95
- return params.length > 0 ? stmt.all(...params) : stmt.all();
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.10",
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.12.0"
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"