Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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\"",
Expand Down
3 changes: 2 additions & 1 deletion server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion server/lib/native-credentials.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 }));
Expand Down
6 changes: 6 additions & 0 deletions server/lib/read-only-database.js
Original file line number Diff line number Diff line change
@@ -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 };
28 changes: 28 additions & 0 deletions server/lib/read-only-database.test.js
Original file line number Diff line number Diff line change
@@ -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 });
}
});
4 changes: 2 additions & 2 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading