diff --git a/CHANGELOG.md b/CHANGELOG.md index 2638591..357b895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [2.5.1] - 2026-10-24 + +### Fixed +- Open credential and usage databases with node:sqlite’s supported `readOnly` option; the lowercase spelling was ignored. +- Add a real SQLite regression proving writes and missing-database creation are rejected. + ## [2.5.0] - 2026-10-24 ### Fixed diff --git a/package.json b/package.json index eb61cc1..04a49ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-studio-server", - "version": "2.5.0", + "version": "2.5.1", "private": false, "scripts": { "start": "concurrently \"npm run server\" \"npm run client\"", diff --git a/server/index.js b/server/index.js index 60505a2..c38b9ec 100644 --- a/server/index.js +++ b/server/index.js @@ -10,6 +10,7 @@ try { ({ DatabaseSync } = require('node:sqlite')); } catch {} const { spawn, exec, execSync } = require('child_process'); const yaml = require('js-yaml'); const configProviders = require('./lib/config-providers'); +const { openReadOnlyDatabase } = require('./lib/read-only-database'); const { readCliConfig, writeCliConfig } = require('./lib/cli-config'); const { readNativeCredentialMetadata } = require('./lib/native-credentials'); const { pluginName, isNativeAgent, hasNativeMcp, readConfigMaps, agentView, mergeNativeAgentUpdate } = require('./lib/opencode-compat'); @@ -5052,7 +5053,7 @@ function tryReadUsageFromSqlite(opts) { const { dbPath, projectIdFilter, min, max, granularity } = opts; let db; try { - db = new DatabaseSync(dbPath, { readonly: true }); + db = openReadOnlyDatabase(DatabaseSync, dbPath); const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(row => row.name)); const native = tables.has('session_message') && tables.has('session_v2'); diff --git a/server/lib/native-credentials.js b/server/lib/native-credentials.js index 8ffd53a..76c3fa3 100644 --- a/server/lib/native-credentials.js +++ b/server/lib/native-credentials.js @@ -1,5 +1,6 @@ const fs = require('node:fs'); const path = require('node:path'); +const { openReadOnlyDatabase } = require('./read-only-database'); // Metadata only. Never select credential.value or rewrite OpenCode's database. function readNativeCredentialMetadata(dataDirectories, DatabaseSync) { @@ -9,7 +10,7 @@ function readNativeCredentialMetadata(dataDirectories, DatabaseSync) { if (!fs.existsSync(file)) continue; let db; try { - db = new DatabaseSync(file, { readonly: true }); + db = openReadOnlyDatabase(DatabaseSync, file); if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='credential'").get()) continue; const credentials = db.prepare('SELECT id, integration_id, label, active FROM credential ORDER BY label, id').all() .map(row => ({ id: row.id, integrationID: row.integration_id, label: row.label, active: !!row.active })); diff --git a/server/lib/read-only-database.js b/server/lib/read-only-database.js new file mode 100644 index 0000000..7cd67a4 --- /dev/null +++ b/server/lib/read-only-database.js @@ -0,0 +1,6 @@ +// node:sqlite uses camel-case readOnly. An unknown `readonly` option is ignored. +function openReadOnlyDatabase(DatabaseSync, file) { + if (!DatabaseSync) throw new Error('SQLite requires a compatible Node runtime'); + return new DatabaseSync(file, { readOnly: true }); +} +module.exports = { openReadOnlyDatabase }; diff --git a/server/lib/read-only-database.test.js b/server/lib/read-only-database.test.js new file mode 100644 index 0000000..fc89724 --- /dev/null +++ b/server/lib/read-only-database.test.js @@ -0,0 +1,28 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { DatabaseSync } = require('node:sqlite'); +const { openReadOnlyDatabase } = require('./read-only-database'); + +test('read-only database rejects writes and cannot create a missing database', () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'studio-readonly-db-')); + const file = path.join(directory, 'fixture.db'); + let reader; + try { + const setup = new DatabaseSync(file); + setup.exec('CREATE TABLE fixture(value TEXT); INSERT INTO fixture VALUES (\'keep\')'); + setup.close(); + reader = openReadOnlyDatabase(DatabaseSync, file); + assert.equal(reader.prepare('SELECT value FROM fixture').get().value, 'keep'); + assert.throws(() => reader.exec("INSERT INTO fixture VALUES ('must fail')"), /readonly|read-only/i); + assert.equal(reader.prepare('SELECT count(*) AS count FROM fixture').get().count, 1); + const missing = path.join(directory, 'missing.db'); + assert.throws(() => openReadOnlyDatabase(DatabaseSync, missing)); + assert.equal(fs.existsSync(missing), false); + } finally { + if (reader) reader.close(); + fs.rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/server/package-lock.json b/server/package-lock.json index ada7968..a1202bb 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-studio-server", - "version": "2.5.0", + "version": "2.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-studio-server", - "version": "2.5.0", + "version": "2.5.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/server/package.json b/server/package.json index 95e4f70..d1bf3ef 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "opencode-studio-server", - "version": "2.5.0", + "version": "2.5.1", "description": "Backend server for OpenCode Studio - manages opencode configurations", "main": "index.js", "bin": {