From 11dfb8db58f751abee0d35e77ef90efae4349aa2 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Wed, 14 Jan 2026 16:30:51 -0800 Subject: [PATCH 1/5] feat: drizzle support with custom migration --- .gitignore | 3 + apps/server/drizzle.config.ts | 15 ++ apps/server/package.json | 27 ++- apps/server/scripts/db/sync-schema.ts | 126 +++++++++++ apps/server/src/lib/db/client.ts | 15 ++ apps/server/src/lib/db/index.ts | 40 +++- apps/server/src/lib/db/migrations/index.ts | 75 +++++++ apps/server/src/lib/db/migrations/versions.ts | 72 ++++++ apps/server/src/lib/db/schema.ts | 27 --- .../server/src/lib/db/schema/conversations.ts | 27 +++ apps/server/src/lib/db/schema/identity.ts | 15 ++ apps/server/src/lib/db/schema/index.ts | 9 + apps/server/src/lib/db/schema/rate-limiter.ts | 14 ++ apps/server/src/main.ts | 11 +- .../agent/rate-limiter.integration.test.ts | 2 +- apps/server/tests/lib/db/migrations.test.ts | 132 +++++++++++ bun.lock | 207 +++++++++++++++++- 17 files changed, 764 insertions(+), 53 deletions(-) create mode 100644 apps/server/drizzle.config.ts create mode 100644 apps/server/scripts/db/sync-schema.ts create mode 100644 apps/server/src/lib/db/client.ts create mode 100644 apps/server/src/lib/db/migrations/index.ts create mode 100644 apps/server/src/lib/db/migrations/versions.ts delete mode 100644 apps/server/src/lib/db/schema.ts create mode 100644 apps/server/src/lib/db/schema/conversations.ts create mode 100644 apps/server/src/lib/db/schema/identity.ts create mode 100644 apps/server/src/lib/db/schema/index.ts create mode 100644 apps/server/src/lib/db/schema/rate-limiter.ts create mode 100644 apps/server/tests/lib/db/migrations.test.ts diff --git a/.gitignore b/.gitignore index 1393eeab..f39ee93b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ lerna-debug.log* browseros.db browseros.db-* +# drizzle-kit generated files (only needed during dev) +**/drizzle/ + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/apps/server/drizzle.config.ts b/apps/server/drizzle.config.ts new file mode 100644 index 00000000..153bfe21 --- /dev/null +++ b/apps/server/drizzle.config.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + out: './drizzle', + schema: './src/lib/db/schema/index.ts', + dialect: 'sqlite', + dbCredentials: { + url: process.env.DB_PATH || 'file:./browseros.db', + }, +}) diff --git a/apps/server/package.json b/apps/server/package.json index 0a6c8883..454c59fd 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -17,7 +17,11 @@ "test:integration": "bun run test:cleanup && bun --env-file=.env.development test tests/server.integration.test.ts", "test:sdk": "bun run test:cleanup && bun --env-file=.env.development test tests/sdk", "test:cleanup": "./tests/__helpers__/cleanup.sh", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "db:sync": "bun scripts/db/sync-schema.ts", + "db:generate": "bunx drizzle-kit generate", + "db:studio": "bunx drizzle-kit studio", + "db:push": "bunx drizzle-kit push" }, "exports": { ".": { @@ -26,8 +30,6 @@ } }, "dependencies": { - "@browseros/shared": "workspace:*", - "@browseros-ai/agent-sdk": "workspace:*", "@ai-sdk/amazon-bedrock": "^3.0.59", "@ai-sdk/anthropic": "^2.0.47", "@ai-sdk/azure": "^2.0.74", @@ -36,9 +38,13 @@ "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "2.0.0", "@ai-sdk/ui-utils": "^1.2.11", + "@browseros-ai/agent-sdk": "workspace:*", + "@browseros/shared": "workspace:*", "@google/gemini-cli-core": "^0.16.0", "@google/genai": "1.30.0", + "@hono/mcp": "^0.2.3", "@hono/node-server": "^1.19.6", + "@hono/zod-validator": "^0.4.3", "@modelcontextprotocol/sdk": "^1.25.2", "@openrouter/ai-sdk-provider": "^1.5.2", "@sentry/bun": "^10.31.0", @@ -46,23 +52,24 @@ "commander": "^14.0.1", "core-js": "3.45.1", "debug": "4.4.3", + "drizzle-orm": "^0.45.1", + "eventsource-parser": "^3.0.0", "hono": "^4.6.0", - "@hono/mcp": "^0.2.3", - "@hono/zod-validator": "^0.4.3", + "pino": "^9.6.0", "posthog-node": "^4.17.0", "puppeteer-core": "24.23.0", "ws": "^8.18.0", - "zod": "^3.24.2", - "pino": "^9.6.0", - "eventsource-parser": "^3.0.0" + "zod": "^3.24.2" }, "devDependencies": { - "async-mutex": "^0.5.0", - "pino-pretty": "^13.0.0", + "@libsql/client": "^0.17.0", "@types/bun": "1.3.5", "@types/debug": "^4.1.12", "@types/node": "^24.3.3", "@types/ws": "^8.5.13", + "async-mutex": "^0.5.0", + "drizzle-kit": "^0.31.8", + "pino-pretty": "^13.0.0", "puppeteer": "24.23.0", "typescript": "^5.9.2" }, diff --git a/apps/server/scripts/db/sync-schema.ts b/apps/server/scripts/db/sync-schema.ts new file mode 100644 index 00000000..b4a33ba7 --- /dev/null +++ b/apps/server/scripts/db/sync-schema.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env bun +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Syncs Drizzle schema changes to embedded migrations in versions.ts + * + * This script: + * 1. Runs drizzle-kit generate to detect schema changes + * 2. Reads any new migration SQL files + * 3. Appends them to src/lib/db/migrations/versions.ts with Unix timestamps + */ + +import { execSync } from 'node:child_process' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +const DRIZZLE_DIR = './drizzle' +const VERSIONS_FILE = './src/lib/db/migrations/versions.ts' +const JOURNAL_FILE = join(DRIZZLE_DIR, 'meta/_journal.json') + +interface JournalEntry { + idx: number + version: string + when: number + tag: string + breakpoints: boolean +} + +interface Journal { + version: string + dialect: string + entries: JournalEntry[] +} + +function getExistingMigrationNames(): Set { + if (!existsSync(VERSIONS_FILE)) { + return new Set() + } + + const content = readFileSync(VERSIONS_FILE, 'utf-8') + const nameMatches = content.matchAll(/name:\s*['"]([^'"]+)['"]/g) + return new Set([...nameMatches].map((m) => m[1])) +} + +function generateMigrationEntry(name: string, sql: string): string { + const version = Math.floor(Date.now() / 1000) + const escapedSql = sql.replace(/`/g, '\\`').replace(/\$/g, '\\$') + + return ` { + version: ${version}, + name: '${name}', + up: \` +${escapedSql} + \`, + },` +} + +function appendMigration(entry: string): void { + const content = readFileSync(VERSIONS_FILE, 'utf-8') + + const lastBracketIndex = content.lastIndexOf(']') + if (lastBracketIndex === -1) { + throw new Error('Could not find migrations array in versions.ts') + } + + const before = content.slice(0, lastBracketIndex) + const after = content.slice(lastBracketIndex) + + const needsNewline = !before.trim().endsWith(',') + const newContent = before + (needsNewline ? '\n' : '') + entry + '\n' + after + + writeFileSync(VERSIONS_FILE, newContent) +} + +async function main() { + console.log('🔄 Checking for schema changes...') + + try { + execSync('bunx drizzle-kit generate', { + stdio: 'pipe', + cwd: process.cwd(), + }) + } catch { + // drizzle-kit returns non-zero if no changes, which is fine + } + + if (!existsSync(JOURNAL_FILE)) { + console.log('✅ No migrations to sync') + return + } + + const journal: Journal = JSON.parse(readFileSync(JOURNAL_FILE, 'utf-8')) + const existingNames = getExistingMigrationNames() + + const newMigrations = journal.entries.filter( + (entry) => !existingNames.has(entry.tag), + ) + + if (newMigrations.length === 0) { + console.log('✅ Schema is up to date') + return + } + + console.log(`📝 Found ${newMigrations.length} new migration(s)`) + + for (const migration of newMigrations) { + const sqlFile = join(DRIZZLE_DIR, `${migration.tag}.sql`) + + if (!existsSync(sqlFile)) { + console.warn(`⚠️ SQL file not found: ${sqlFile}`) + continue + } + + const sql = readFileSync(sqlFile, 'utf-8') + const entry = generateMigrationEntry(migration.tag, sql) + + appendMigration(entry) + console.log(` ✅ Added migration: ${migration.tag}`) + } + + console.log('✨ Schema sync complete') +} + +main().catch(console.error) diff --git a/apps/server/src/lib/db/client.ts b/apps/server/src/lib/db/client.ts new file mode 100644 index 00000000..d4973899 --- /dev/null +++ b/apps/server/src/lib/db/client.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { Database } from 'bun:sqlite' +import { drizzle } from 'drizzle-orm/bun-sqlite' + +import * as schema from './schema' + +export type DrizzleDb = ReturnType + +export function createDrizzleClient(sqliteDb: Database) { + return drizzle(sqliteDb, { schema }) +} diff --git a/apps/server/src/lib/db/index.ts b/apps/server/src/lib/db/index.ts index 88e6d50f..79090f91 100644 --- a/apps/server/src/lib/db/index.ts +++ b/apps/server/src/lib/db/index.ts @@ -5,20 +5,40 @@ */ import { Database } from 'bun:sqlite' -import { initSchema } from './schema' +import { logger } from '../logger' +import { createDrizzleClient, type DrizzleDb } from './client' +import { runMigrations } from './migrations' let db: Database | null = null +let drizzleDb: DrizzleDb | null = null -export function initializeDb(dbPath: string): Database { - if (!db) { - db = new Database(dbPath) - db.exec('PRAGMA journal_mode = WAL') - initSchema(db) +export function initializeDb(dbPath: string): DrizzleDb { + if (drizzleDb) { + return drizzleDb } - return db + + logger.info('Initializing database', { path: dbPath }) + + db = new Database(dbPath) + db.exec('PRAGMA journal_mode = WAL') + db.exec('PRAGMA foreign_keys = ON') + + runMigrations(db) + + drizzleDb = createDrizzleClient(db) + + logger.info('Database initialized successfully') + return drizzleDb +} + +export function getDb(): DrizzleDb { + if (!drizzleDb) { + throw new Error('Database not initialized. Call initializeDb() first.') + } + return drizzleDb } -export function getDb(): Database { +export function getRawDb(): Database { if (!db) { throw new Error('Database not initialized. Call initializeDb() first.') } @@ -29,5 +49,9 @@ export function closeDb(): void { if (db) { db.close() db = null + drizzleDb = null } } + +export * from './schema' +export type { DrizzleDb } diff --git a/apps/server/src/lib/db/migrations/index.ts b/apps/server/src/lib/db/migrations/index.ts new file mode 100644 index 00000000..ae25d221 --- /dev/null +++ b/apps/server/src/lib/db/migrations/index.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { Database } from 'bun:sqlite' + +import { logger } from '../../logger' +import { migrations } from './versions' + +interface AppliedMigration { + version: number +} + +function splitStatements(sql: string): string[] { + return sql + .split('--> statement-breakpoint') + .map((s) => s.trim()) + .filter((s) => s.length > 0) +} + +export function runMigrations(db: Database): void { + db.exec(` + CREATE TABLE IF NOT EXISTS _migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + `) + + const applied = db + .prepare('SELECT version FROM _migrations') + .all() as AppliedMigration[] + + const appliedVersions = new Set(applied.map((m) => m.version)) + + const pending = migrations + .filter((m) => !appliedVersions.has(m.version)) + .sort((a, b) => a.version - b.version) + + if (pending.length === 0) { + logger.debug('No pending migrations') + return + } + + logger.info(`Running ${pending.length} migration(s)...`) + + for (const migration of pending) { + logger.info(`Applying migration ${migration.version}: ${migration.name}`) + + const transaction = db.transaction(() => { + const statements = splitStatements(migration.up) + for (const statement of statements) { + db.exec(statement) + } + db.prepare('INSERT INTO _migrations (version, name) VALUES (?, ?)').run( + migration.version, + migration.name, + ) + }) + + try { + transaction() + logger.info(`Migration ${migration.version} applied successfully`) + } catch (error) { + logger.error(`Migration ${migration.version} failed`, { + name: migration.name, + error: error instanceof Error ? error.message : String(error), + }) + throw error + } + } + + logger.info('All migrations applied successfully') +} diff --git a/apps/server/src/lib/db/migrations/versions.ts b/apps/server/src/lib/db/migrations/versions.ts new file mode 100644 index 00000000..247a4249 --- /dev/null +++ b/apps/server/src/lib/db/migrations/versions.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export interface Migration { + version: number + name: string + up: string +} + +export const migrations: Migration[] = [ + { + version: 1736864400, + name: 'initial_schema', + up: ` + -- Identity table (single row for this installation) + CREATE TABLE IF NOT EXISTS identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + browseros_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + -- Rate limiter table (conversation_id as PK for deduplication) + CREATE TABLE IF NOT EXISTS rate_limiter ( + id TEXT PRIMARY KEY, + browseros_id TEXT NOT NULL, + provider TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + -- Migrations tracking table + CREATE TABLE IF NOT EXISTS _migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + `, + }, + { + version: 1736950800, + name: 'conversations', + up: ` + -- Conversations table + CREATE TABLE IF NOT EXISTS conversations ( + id TEXT PRIMARY KEY, + browseros_id TEXT NOT NULL, + provider TEXT NOT NULL, + model TEXT, + title TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + -- Messages table + CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), + content TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + -- Indexes for common queries + CREATE INDEX IF NOT EXISTS idx_conversations_browseros_id ON conversations(browseros_id); + CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); + CREATE INDEX IF NOT EXISTS idx_rate_limiter_browseros_id_date + ON rate_limiter(browseros_id, created_at); + `, + }, +] diff --git a/apps/server/src/lib/db/schema.ts b/apps/server/src/lib/db/schema.ts deleted file mode 100644 index 2e0412c1..00000000 --- a/apps/server/src/lib/db/schema.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import type { Database } from 'bun:sqlite' - -// id is the conversation_id - using it as PK ensures same conversation is only counted once -const RATE_LIMITER_TABLE = ` -CREATE TABLE IF NOT EXISTS rate_limiter ( - id TEXT PRIMARY KEY, - browseros_id TEXT NOT NULL, - provider TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -)` - -const IDENTITY_TABLE = ` -CREATE TABLE IF NOT EXISTS identity ( - id INTEGER PRIMARY KEY CHECK (id = 1), - browseros_id TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -)` - -export function initSchema(db: Database): void { - db.exec(RATE_LIMITER_TABLE) - db.exec(IDENTITY_TABLE) -} diff --git a/apps/server/src/lib/db/schema/conversations.ts b/apps/server/src/lib/db/schema/conversations.ts new file mode 100644 index 00000000..10eab8ec --- /dev/null +++ b/apps/server/src/lib/db/schema/conversations.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { sql } from 'drizzle-orm' +import { sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const conversations = sqliteTable('conversations', { + id: text('id').primaryKey(), + browserosId: text('browseros_id').notNull(), + provider: text('provider').notNull(), + model: text('model'), + title: text('title'), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), + updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`), +}) + +export const messages = sqliteTable('messages', { + id: text('id').primaryKey(), + conversationId: text('conversation_id') + .notNull() + .references(() => conversations.id, { onDelete: 'cascade' }), + role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(), + content: text('content').notNull(), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), +}) diff --git a/apps/server/src/lib/db/schema/identity.ts b/apps/server/src/lib/db/schema/identity.ts new file mode 100644 index 00000000..c52aaf5a --- /dev/null +++ b/apps/server/src/lib/db/schema/identity.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { sql } from 'drizzle-orm' +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const identity = sqliteTable('identity', { + id: integer('id') + .primaryKey() + .$default(() => 1), + browserosId: text('browseros_id').notNull(), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), +}) diff --git a/apps/server/src/lib/db/schema/index.ts b/apps/server/src/lib/db/schema/index.ts new file mode 100644 index 00000000..a7ac6b74 --- /dev/null +++ b/apps/server/src/lib/db/schema/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export * from './conversations' +export * from './identity' +export * from './rate-limiter' diff --git a/apps/server/src/lib/db/schema/rate-limiter.ts b/apps/server/src/lib/db/schema/rate-limiter.ts new file mode 100644 index 00000000..f0b8bc05 --- /dev/null +++ b/apps/server/src/lib/db/schema/rate-limiter.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { sql } from 'drizzle-orm' +import { sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const rateLimiter = sqliteTable('rate_limiter', { + id: text('id').primaryKey(), + browserosId: text('browseros_id').notNull(), + provider: text('provider').notNull(), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), +}) diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index ef0235c8..74b730cd 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -8,7 +8,6 @@ * Manages server lifecycle: initialization, startup, and shutdown. */ -import type { Database } from 'bun:sqlite' import fs from 'node:fs' import path from 'node:path' import { EXIT_CODES } from '@browseros/shared/constants/exit-codes' @@ -18,7 +17,7 @@ import { McpContext } from './browser/cdp/context' import { ControllerBridge } from './browser/extension/bridge' import { ControllerContext } from './browser/extension/context' import type { ServerConfig } from './config' -import { initializeDb } from './lib/db' +import { type DrizzleDb, getRawDb, initializeDb } from './lib/db' import { identity } from './lib/identity' import { logger } from './lib/logger' import { metrics } from './lib/metrics' @@ -32,7 +31,7 @@ import { VERSION } from './version' export class Application { private config: ServerConfig - private db: Database | null = null + private db: DrizzleDb | null = null constructor(config: ServerConfig) { this.config = config @@ -81,7 +80,7 @@ export class Application { allowRemote: this.config.mcpAllowRemote, browserosId: identity.getBrowserOSId(), executionDir: this.config.executionDir, - rateLimiter: new RateLimiter(this.getDb(), dailyRateLimit), + rateLimiter: new RateLimiter(getRawDb(), dailyRateLimit), codegenServiceUrl: this.config.codegenServiceUrl, }) } catch (error) { @@ -118,7 +117,7 @@ export class Application { identity.initialize({ installId: this.config.instanceInstallId, - db: this.db, + db: getRawDb(), }) const browserosId = identity.getBrowserOSId() @@ -243,7 +242,7 @@ export class Application { logger.info('') } - private getDb(): Database { + private getDb(): DrizzleDb { if (!this.db) { throw new Error( 'Database not initialized. Call initCoreServices() first.', diff --git a/apps/server/tests/agent/rate-limiter.integration.test.ts b/apps/server/tests/agent/rate-limiter.integration.test.ts index 012f7b52..f8648f2b 100644 --- a/apps/server/tests/agent/rate-limiter.integration.test.ts +++ b/apps/server/tests/agent/rate-limiter.integration.test.ts @@ -13,7 +13,7 @@ import { beforeEach, describe, expect, it } from 'bun:test' import { RateLimitError, RateLimiter, -} from '../../src/agent/rate-limiter/rate-limiter' +} from '../../src/lib/rate-limiter/rate-limiter' const DAILY_RATE_LIMIT_TEST = 3 diff --git a/apps/server/tests/lib/db/migrations.test.ts b/apps/server/tests/lib/db/migrations.test.ts new file mode 100644 index 00000000..a79cfc14 --- /dev/null +++ b/apps/server/tests/lib/db/migrations.test.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { Database } from 'bun:sqlite' +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' + +import { runMigrations } from '../../../src/lib/db/migrations' + +describe('migrations', () => { + let db: Database + + beforeEach(() => { + db = new Database(':memory:') + }) + + afterEach(() => { + db.close() + }) + + test('applies all migrations', () => { + runMigrations(db) + + const tables = db + .prepare("SELECT name FROM sqlite_master WHERE type='table'") + .all() as { name: string }[] + + const tableNames = tables.map((t) => t.name) + expect(tableNames).toContain('identity') + expect(tableNames).toContain('rate_limiter') + expect(tableNames).toContain('conversations') + expect(tableNames).toContain('messages') + expect(tableNames).toContain('_migrations') + }) + + test('is idempotent', () => { + runMigrations(db) + runMigrations(db) + + const migrations = db.prepare('SELECT * FROM _migrations').all() + + expect(migrations.length).toBeGreaterThan(0) + }) + + test('records applied migrations', () => { + runMigrations(db) + + const applied = db + .prepare('SELECT version, name FROM _migrations ORDER BY version') + .all() as { version: number; name: string }[] + + expect(applied.length).toBeGreaterThanOrEqual(2) + expect(applied[0].name).toBe('initial_schema') + expect(applied[1].name).toBe('conversations') + expect(applied[0].version).toBeGreaterThan(1700000000) + }) + + test('creates indexes', () => { + runMigrations(db) + + const indexes = db + .prepare("SELECT name FROM sqlite_master WHERE type='index'") + .all() as { name: string }[] + + const indexNames = indexes.map((i) => i.name) + expect(indexNames).toContain('idx_conversations_browseros_id') + expect(indexNames).toContain('idx_messages_conversation_id') + expect(indexNames).toContain('idx_rate_limiter_browseros_id_date') + }) + + test('identity table has CHECK constraint', () => { + runMigrations(db) + + db.exec("INSERT INTO identity (id, browseros_id) VALUES (1, 'test-id')") + + expect(() => { + db.exec( + "INSERT INTO identity (id, browseros_id) VALUES (2, 'another-id')", + ) + }).toThrow() + }) + + test('messages role has CHECK constraint', () => { + runMigrations(db) + + db.exec(` + INSERT INTO conversations (id, browseros_id, provider) + VALUES ('conv-1', 'test-browseros', 'anthropic') + `) + + db.exec(` + INSERT INTO messages (id, conversation_id, role, content) + VALUES ('msg-1', 'conv-1', 'user', 'Hello') + `) + + expect(() => { + db.exec(` + INSERT INTO messages (id, conversation_id, role, content) + VALUES ('msg-2', 'conv-1', 'invalid_role', 'Oops') + `) + }).toThrow() + }) + + test('messages cascade delete when conversation deleted', () => { + runMigrations(db) + db.exec('PRAGMA foreign_keys = ON') + + db.exec(` + INSERT INTO conversations (id, browseros_id, provider) + VALUES ('conv-1', 'test-browseros', 'anthropic') + `) + + db.exec(` + INSERT INTO messages (id, conversation_id, role, content) + VALUES ('msg-1', 'conv-1', 'user', 'Hello'), + ('msg-2', 'conv-1', 'assistant', 'Hi there') + `) + + const messagesBefore = db + .prepare('SELECT COUNT(*) as count FROM messages') + .get() as { count: number } + expect(messagesBefore.count).toBe(2) + + db.exec("DELETE FROM conversations WHERE id = 'conv-1'") + + const messagesAfter = db + .prepare('SELECT COUNT(*) as count FROM messages') + .get() as { count: number } + expect(messagesAfter.count).toBe(0) + }) +}) diff --git a/bun.lock b/bun.lock index 2281bd61..70ce8483 100644 --- a/bun.lock +++ b/bun.lock @@ -117,7 +117,7 @@ }, "apps/server": { "name": "@browseros/server", - "version": "0.0.40", + "version": "0.0.41", "bin": { "browseros-server": "./src/index.ts", }, @@ -144,6 +144,7 @@ "commander": "^14.0.1", "core-js": "3.45.1", "debug": "4.4.3", + "drizzle-orm": "^0.45.1", "eventsource-parser": "^3.0.0", "hono": "^4.6.0", "pino": "^9.6.0", @@ -153,11 +154,13 @@ "zod": "^3.24.2", }, "devDependencies": { + "@libsql/client": "^0.17.0", "@types/bun": "1.3.5", "@types/debug": "^4.1.12", "@types/node": "^24.3.3", "@types/ws": "^8.5.13", "async-mutex": "^0.5.0", + "drizzle-kit": "^0.31.8", "pino-pretty": "^13.0.0", "puppeteer": "24.23.0", "typescript": "^5.9.2", @@ -347,6 +350,8 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], @@ -381,6 +386,10 @@ "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -515,6 +524,32 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="], + + "@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.0", "", {}, "sha512-HLomZXMmrCFHSRKESF5vklAKsDY7/fsT/ZhqCu3V0UoW/Qbv8wxmO4W9bx4KnCCF2Zak4yuk+AGraK/bPmI4kA=="], "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], @@ -555,6 +590,8 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -1107,6 +1144,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="], @@ -1419,10 +1458,16 @@ "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], + "better-sqlite3": ["better-sqlite3@12.6.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -1495,6 +1540,8 @@ "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "chroma-js": ["chroma-js@3.2.0", "", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], "chrome-devtools-mcp": ["chrome-devtools-mcp@0.12.1", "", { "bin": { "chrome-devtools-mcp": "build/src/index.js" } }, "sha512-QREfGxJVVlBrjKdyis9px6UHyXix+Rre9nCkqX7CY7GsU8c6azOwwV8inQB8E3h2/QGqi4sCSF8fmjfAvmE07Q=="], @@ -1589,6 +1636,8 @@ "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], @@ -1743,6 +1792,10 @@ "downshift": ["downshift@9.0.13", "", { "dependencies": { "@babel/runtime": "^7.24.5", "compute-scroll-into-view": "^3.1.0", "prop-types": "^15.8.1", "react-is": "18.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "react": ">=16.12.0" } }, "sha512-fPV+K5jwEzfEAhNhprgCmpWQ23MKwKNzdbtK0QQFiw4hbFcKhMeGB+ccorfWJzmsLR5Dty+CmLDduWlIs74G/w=="], + "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], @@ -1801,6 +1854,8 @@ "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], @@ -1853,6 +1908,8 @@ "execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -1899,6 +1956,8 @@ "file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "filesize": ["filesize@11.0.13", "", {}, "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1939,6 +1998,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -1969,6 +2030,8 @@ "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], @@ -1977,6 +2040,8 @@ "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -2281,6 +2346,8 @@ "leva": ["leva@0.10.1", "", { "dependencies": { "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA=="], + "libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="], @@ -2525,6 +2592,8 @@ "mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], "mnemonist": ["mnemonist@0.40.3", "", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ=="], @@ -2547,6 +2616,8 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -2555,6 +2626,8 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="], + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -2731,6 +2804,8 @@ "preact": ["preact@10.28.1", "", {}, "sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -2741,6 +2816,8 @@ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + "promise-toolbox": ["promise-toolbox@0.21.0", "", { "dependencies": { "make-error": "^1.3.2" } }, "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -2995,6 +3072,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@4.0.2", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -3087,6 +3166,10 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "simple-git": ["simple-git@3.30.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -3251,6 +3334,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], @@ -3515,6 +3600,8 @@ "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@google-cloud/logging/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@google/gemini-cli-core/@google/genai": ["@google/genai@1.16.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw=="], @@ -3535,6 +3622,8 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], "@lobehub/fluent-emoji/react-layout-kit": ["react-layout-kit@1.9.2", "", { "dependencies": { "@babel/runtime": "^7", "@emotion/css": "^11" }, "peerDependencies": { "react": ">=18" } }, "sha512-fzmrwMBNGIAiDIrdFMV3NvJhUNl01QC9EMcI8SP7osg51N4j+z6w4tx9i2yWxEEXZ2armLV6EtkFd3KST8PYiA=="], @@ -3799,6 +3888,10 @@ "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -3849,6 +3942,8 @@ "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -3925,6 +4020,8 @@ "leva/zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], @@ -3971,6 +4068,8 @@ "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -4005,6 +4104,8 @@ "send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "simple-get/decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], @@ -4031,6 +4132,8 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tunnel-agent/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "unimport/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], @@ -4071,6 +4174,50 @@ "@browseros/server/@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@google/gemini-cli-core/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "@google/gemini-cli-core/@modelcontextprotocol/sdk/zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="], @@ -4237,6 +4384,58 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="], @@ -4259,8 +4458,12 @@ "pkg-dir/find-up/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "simple-get/decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -4309,6 +4512,8 @@ "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], From bf46caa7b3706ca95803a6071f2a3b7a8e55e2e2 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Wed, 14 Jan 2026 16:38:16 -0800 Subject: [PATCH 2/5] feat: migrate rater limiter to drizzle and checkin drizzle/ package --- .gitignore | 3 - .../server/drizzle/0000_messy_rafael_vega.sql | 31 +++ apps/server/drizzle/meta/0000_snapshot.json | 206 ++++++++++++++++++ apps/server/drizzle/meta/_journal.json | 13 ++ apps/server/scripts/db/sync-schema.ts | 2 +- apps/server/src/lib/db/index.ts | 7 - apps/server/src/lib/db/migrations/index.ts | 13 ++ apps/server/src/lib/identity.ts | 33 ++- .../src/lib/rate-limiter/rate-limiter.ts | 52 ++--- apps/server/src/main.ts | 18 +- 10 files changed, 315 insertions(+), 63 deletions(-) create mode 100644 apps/server/drizzle/0000_messy_rafael_vega.sql create mode 100644 apps/server/drizzle/meta/0000_snapshot.json create mode 100644 apps/server/drizzle/meta/_journal.json diff --git a/.gitignore b/.gitignore index f39ee93b..1393eeab 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,6 @@ lerna-debug.log* browseros.db browseros.db-* -# drizzle-kit generated files (only needed during dev) -**/drizzle/ - # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/apps/server/drizzle/0000_messy_rafael_vega.sql b/apps/server/drizzle/0000_messy_rafael_vega.sql new file mode 100644 index 00000000..99a57eca --- /dev/null +++ b/apps/server/drizzle/0000_messy_rafael_vega.sql @@ -0,0 +1,31 @@ +CREATE TABLE `identity` ( + `id` integer PRIMARY KEY NOT NULL, + `browseros_id` text NOT NULL, + `created_at` text DEFAULT (datetime('now')) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `rate_limiter` ( + `id` text PRIMARY KEY NOT NULL, + `browseros_id` text NOT NULL, + `provider` text NOT NULL, + `created_at` text DEFAULT (datetime('now')) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `conversations` ( + `id` text PRIMARY KEY NOT NULL, + `browseros_id` text NOT NULL, + `provider` text NOT NULL, + `model` text, + `title` text, + `created_at` text DEFAULT (datetime('now')) NOT NULL, + `updated_at` text DEFAULT (datetime('now')) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `messages` ( + `id` text PRIMARY KEY NOT NULL, + `conversation_id` text NOT NULL, + `role` text NOT NULL, + `content` text NOT NULL, + `created_at` text DEFAULT (datetime('now')) NOT NULL, + FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/apps/server/drizzle/meta/0000_snapshot.json b/apps/server/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..76896225 --- /dev/null +++ b/apps/server/drizzle/meta/0000_snapshot.json @@ -0,0 +1,206 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1476beea-4c2e-41e4-8be4-3a5e1b7a9a6e", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "identity": { + "name": "identity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "browseros_id": { + "name": "browseros_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limiter": { + "name": "rate_limiter", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "browseros_id": { + "name": "browseros_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "browseros_id": { + "name": "browseros_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json new file mode 100644 index 00000000..ad1cbe7b --- /dev/null +++ b/apps/server/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1768436911212, + "tag": "0000_messy_rafael_vega", + "breakpoints": true + } + ] +} diff --git a/apps/server/scripts/db/sync-schema.ts b/apps/server/scripts/db/sync-schema.ts index b4a33ba7..68553d00 100644 --- a/apps/server/scripts/db/sync-schema.ts +++ b/apps/server/scripts/db/sync-schema.ts @@ -69,7 +69,7 @@ function appendMigration(entry: string): void { const after = content.slice(lastBracketIndex) const needsNewline = !before.trim().endsWith(',') - const newContent = before + (needsNewline ? '\n' : '') + entry + '\n' + after + const newContent = `${before}${needsNewline ? '\n' : ''}${entry}\n${after}` writeFileSync(VERSIONS_FILE, newContent) } diff --git a/apps/server/src/lib/db/index.ts b/apps/server/src/lib/db/index.ts index 79090f91..c06d603a 100644 --- a/apps/server/src/lib/db/index.ts +++ b/apps/server/src/lib/db/index.ts @@ -38,13 +38,6 @@ export function getDb(): DrizzleDb { return drizzleDb } -export function getRawDb(): Database { - if (!db) { - throw new Error('Database not initialized. Call initializeDb() first.') - } - return db -} - export function closeDb(): void { if (db) { db.close() diff --git a/apps/server/src/lib/db/migrations/index.ts b/apps/server/src/lib/db/migrations/index.ts index ae25d221..6f364b8d 100644 --- a/apps/server/src/lib/db/migrations/index.ts +++ b/apps/server/src/lib/db/migrations/index.ts @@ -2,6 +2,19 @@ * @license * Copyright 2025 BrowserOS * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Embedded SQL migrations for Bun compile compatibility. + * + * Migrations are stored as strings in versions.ts rather than separate .sql files + * so they can be bundled into the compiled binary. This removes filesystem + * dependencies at runtime, making the server fully self-contained. + * + * To add new migrations: + * 1. Update schema files in src/lib/db/schema/ + * 2. Run `bun run db:sync` to detect changes and append to versions.ts + * 3. Review the generated SQL in versions.ts + * + * The drizzle/ folder contains drizzle-kit output for reference (not packaged). */ import type { Database } from 'bun:sqlite' diff --git a/apps/server/src/lib/identity.ts b/apps/server/src/lib/identity.ts index 42d9c9b8..30af8c4b 100644 --- a/apps/server/src/lib/identity.ts +++ b/apps/server/src/lib/identity.ts @@ -3,15 +3,18 @@ * Copyright 2025 BrowserOS * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Database } from 'bun:sqlite' +import { eq } from 'drizzle-orm' + +import type { DrizzleDb } from './db' +import { identity as identityTable } from './db/schema' export interface IdentityConfig { installId?: string - db: Database + db: DrizzleDb } class IdentityService { - private browserOSId: string | null = null // Unique identifier for the BrowserOS instance + private browserOSId: string | null = null initialize(config: IdentityConfig): void { const { installId, db } = config @@ -34,18 +37,24 @@ class IdentityService { return this.browserOSId !== null } - private loadFromDb(db: Database): string | null { - const stmt = db.prepare('SELECT browseros_id FROM identity WHERE id = 1') - const row = stmt.get() as { browseros_id: string } | null - return row?.browseros_id ?? null + private loadFromDb(db: DrizzleDb): string | null { + const row = db + .select({ browserosId: identityTable.browserosId }) + .from(identityTable) + .where(eq(identityTable.id, 1)) + .get() + return row?.browserosId ?? null } - private generateAndSave(db: Database): string { + private generateAndSave(db: DrizzleDb): string { const browserosId = crypto.randomUUID() - const stmt = db.prepare( - 'INSERT OR REPLACE INTO identity (id, browseros_id) VALUES (1, ?)', - ) - stmt.run(browserosId) + db.insert(identityTable) + .values({ id: 1, browserosId }) + .onConflictDoUpdate({ + target: identityTable.id, + set: { browserosId }, + }) + .run() return browserosId } } diff --git a/apps/server/src/lib/rate-limiter/rate-limiter.ts b/apps/server/src/lib/rate-limiter/rate-limiter.ts index bcd10f77..128cbd52 100644 --- a/apps/server/src/lib/rate-limiter/rate-limiter.ts +++ b/apps/server/src/lib/rate-limiter/rate-limiter.ts @@ -4,9 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Database } from 'bun:sqlite' import { RATE_LIMITS } from '@browseros/shared/constants/limits' +import { count, sql } from 'drizzle-orm' +import type { DrizzleDb } from '../db' +import { rateLimiter } from '../db/schema' import { logger } from '../logger' import { metrics } from '../metrics' @@ -19,55 +21,53 @@ export interface RecordParams { } export class RateLimiter { - private countStmt: ReturnType - private insertStmt: ReturnType + private db: DrizzleDb private dailyRateLimit: number constructor( - db: Database, + db: DrizzleDb, dailyRateLimit: number = RATE_LIMITS.DEFAULT_DAILY, ) { + this.db = db this.dailyRateLimit = dailyRateLimit - this.countStmt = db.prepare(` - SELECT COUNT(*) as count - FROM rate_limiter - WHERE browseros_id = ? - AND date(created_at) = date('now') - `) - - // INSERT OR IGNORE: duplicate conversation_ids are silently ignored - // This ensures the same conversation is only counted once for rate limiting - this.insertStmt = db.prepare(` - INSERT OR IGNORE INTO rate_limiter - (id, browseros_id, provider) - VALUES (?, ?, ?) - `) } check(browserosId: string): void { - const count = this.getTodayCount(browserosId) - if (count >= this.dailyRateLimit) { + const todayCount = this.getTodayCount(browserosId) + if (todayCount >= this.dailyRateLimit) { logger.warn('Rate limit exceeded', { browserosId, - count, + count: todayCount, dailyRateLimit: this.dailyRateLimit, }) metrics.log('rate_limit.triggered', { - count, + count: todayCount, daily_limit: this.dailyRateLimit, }) - throw new RateLimitError(count, this.dailyRateLimit) + throw new RateLimitError(todayCount, this.dailyRateLimit) } } record(params: RecordParams): void { const { conversationId, browserosId, provider } = params - this.insertStmt.run(conversationId, browserosId, provider) + // INSERT OR IGNORE: duplicate conversation_ids are silently ignored + // This ensures the same conversation is only counted once for rate limiting + this.db + .insert(rateLimiter) + .values({ id: conversationId, browserosId, provider }) + .onConflictDoNothing() + .run() } private getTodayCount(browserosId: string): number { - const row = this.countStmt.get(browserosId) as { count: number } | null - return row?.count ?? 0 + const result = this.db + .select({ count: count() }) + .from(rateLimiter) + .where( + sql`${rateLimiter.browserosId} = ${browserosId} AND date(${rateLimiter.createdAt}) = date('now')`, + ) + .get() + return result?.count ?? 0 } } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 74b730cd..3f7ef3ec 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -17,7 +17,7 @@ import { McpContext } from './browser/cdp/context' import { ControllerBridge } from './browser/extension/bridge' import { ControllerContext } from './browser/extension/context' import type { ServerConfig } from './config' -import { type DrizzleDb, getRawDb, initializeDb } from './lib/db' +import { getDb, initializeDb } from './lib/db' import { identity } from './lib/identity' import { logger } from './lib/logger' import { metrics } from './lib/metrics' @@ -31,7 +31,6 @@ import { VERSION } from './version' export class Application { private config: ServerConfig - private db: DrizzleDb | null = null constructor(config: ServerConfig) { this.config = config @@ -80,7 +79,7 @@ export class Application { allowRemote: this.config.mcpAllowRemote, browserosId: identity.getBrowserOSId(), executionDir: this.config.executionDir, - rateLimiter: new RateLimiter(getRawDb(), dailyRateLimit), + rateLimiter: new RateLimiter(getDb(), dailyRateLimit), codegenServiceUrl: this.config.codegenServiceUrl, }) } catch (error) { @@ -113,11 +112,11 @@ export class Application { this.config.executionDir || this.config.resourcesDir, 'browseros.db', ) - this.db = initializeDb(dbPath) + initializeDb(dbPath) identity.initialize({ installId: this.config.instanceInstallId, - db: getRawDb(), + db: getDb(), }) const browserosId = identity.getBrowserOSId() @@ -241,13 +240,4 @@ export class Application { logger.info(` HTTP Server: http://127.0.0.1:${this.config.serverPort}`) logger.info('') } - - private getDb(): DrizzleDb { - if (!this.db) { - throw new Error( - 'Database not initialized. Call initCoreServices() first.', - ) - } - return this.db - } } From 76d70c6900897b88aa30124b205acf3bd5de9435 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Wed, 14 Jan 2026 16:59:05 -0800 Subject: [PATCH 3/5] feat: if migrate fails, nuke db and create fresh --- apps/server/src/lib/db/index.ts | 53 ++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/server/src/lib/db/index.ts b/apps/server/src/lib/db/index.ts index c06d603a..18aa50aa 100644 --- a/apps/server/src/lib/db/index.ts +++ b/apps/server/src/lib/db/index.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { Database } from 'bun:sqlite' +import { existsSync, renameSync, unlinkSync } from 'node:fs' import { logger } from '../logger' import { createDrizzleClient, type DrizzleDb } from './client' @@ -12,6 +13,39 @@ import { runMigrations } from './migrations' let db: Database | null = null let drizzleDb: DrizzleDb | null = null +function openAndMigrate(dbPath: string): Database { + const sqliteDb = new Database(dbPath) + sqliteDb.exec('PRAGMA journal_mode = WAL') + sqliteDb.exec('PRAGMA foreign_keys = ON') + runMigrations(sqliteDb) + return sqliteDb +} + +function nukeAndRetry(dbPath: string): Database { + const backupPath = `${dbPath}.bak` + + logger.warn('Backing up corrupted database', { from: dbPath, to: backupPath }) + + // Remove old backup if exists + if (existsSync(backupPath)) { + unlinkSync(backupPath) + } + + // Backup current db + if (existsSync(dbPath)) { + renameSync(dbPath, backupPath) + } + + // Also clean up WAL files + const walPath = `${dbPath}-wal` + const shmPath = `${dbPath}-shm` + if (existsSync(walPath)) unlinkSync(walPath) + if (existsSync(shmPath)) unlinkSync(shmPath) + + logger.info('Creating fresh database') + return openAndMigrate(dbPath) +} + export function initializeDb(dbPath: string): DrizzleDb { if (drizzleDb) { return drizzleDb @@ -19,11 +53,22 @@ export function initializeDb(dbPath: string): DrizzleDb { logger.info('Initializing database', { path: dbPath }) - db = new Database(dbPath) - db.exec('PRAGMA journal_mode = WAL') - db.exec('PRAGMA foreign_keys = ON') + try { + db = openAndMigrate(dbPath) + } catch (error) { + logger.error('Migration failed, attempting recovery', { + error: error instanceof Error ? error.message : String(error), + }) + + // Close if partially opened + if (db) { + db.close() + db = null + } - runMigrations(db) + // Nuke and retry - if this fails, let it crash + db = nukeAndRetry(dbPath) + } drizzleDb = createDrizzleClient(db) From 8f19ca6a96689a80be2f2f1d487ed98707fd7e22 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Wed, 14 Jan 2026 17:28:45 -0800 Subject: [PATCH 4/5] fix: fix drizzle sync issue --- apps/server/drizzle/.synced.json | 3 +++ apps/server/scripts/db/sync-schema.ts | 20 +++++++++++++------- package.json | 4 ++++ 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 apps/server/drizzle/.synced.json diff --git a/apps/server/drizzle/.synced.json b/apps/server/drizzle/.synced.json new file mode 100644 index 00000000..f05a9b69 --- /dev/null +++ b/apps/server/drizzle/.synced.json @@ -0,0 +1,3 @@ +{ + "tags": ["0000_messy_rafael_vega"] +} diff --git a/apps/server/scripts/db/sync-schema.ts b/apps/server/scripts/db/sync-schema.ts index 68553d00..a3d76c90 100644 --- a/apps/server/scripts/db/sync-schema.ts +++ b/apps/server/scripts/db/sync-schema.ts @@ -10,6 +10,7 @@ * 1. Runs drizzle-kit generate to detect schema changes * 2. Reads any new migration SQL files * 3. Appends them to src/lib/db/migrations/versions.ts with Unix timestamps + * 4. Tracks synced drizzle tags in .synced.json to avoid duplicates */ import { execSync } from 'node:child_process' @@ -19,6 +20,7 @@ import { join } from 'node:path' const DRIZZLE_DIR = './drizzle' const VERSIONS_FILE = './src/lib/db/migrations/versions.ts' const JOURNAL_FILE = join(DRIZZLE_DIR, 'meta/_journal.json') +const SYNCED_FILE = join(DRIZZLE_DIR, '.synced.json') interface JournalEntry { idx: number @@ -34,14 +36,16 @@ interface Journal { entries: JournalEntry[] } -function getExistingMigrationNames(): Set { - if (!existsSync(VERSIONS_FILE)) { +function getSyncedTags(): Set { + if (!existsSync(SYNCED_FILE)) { return new Set() } + const data = JSON.parse(readFileSync(SYNCED_FILE, 'utf-8')) + return new Set(data.tags || []) +} - const content = readFileSync(VERSIONS_FILE, 'utf-8') - const nameMatches = content.matchAll(/name:\s*['"]([^'"]+)['"]/g) - return new Set([...nameMatches].map((m) => m[1])) +function saveSyncedTags(tags: Set): void { + writeFileSync(SYNCED_FILE, JSON.stringify({ tags: [...tags] }, null, 2)) } function generateMigrationEntry(name: string, sql: string): string { @@ -92,10 +96,10 @@ async function main() { } const journal: Journal = JSON.parse(readFileSync(JOURNAL_FILE, 'utf-8')) - const existingNames = getExistingMigrationNames() + const syncedTags = getSyncedTags() const newMigrations = journal.entries.filter( - (entry) => !existingNames.has(entry.tag), + (entry) => !syncedTags.has(entry.tag), ) if (newMigrations.length === 0) { @@ -117,9 +121,11 @@ async function main() { const entry = generateMigrationEntry(migration.tag, sql) appendMigration(entry) + syncedTags.add(migration.tag) console.log(` ✅ Added migration: ${migration.tag}`) } + saveSyncedTags(syncedTags) console.log('✨ Schema sync complete') } diff --git a/package.json b/package.json index a1745f79..1c9744dd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ "test:controller": "bun run --filter @browseros/server test:controller", "test:integration": "bun run --filter @browseros/server test:integration", "typecheck": "bun run --filter '*' typecheck", + "db:sync": "bun run --filter @browseros/server db:sync", + "db:generate": "bun run --filter @browseros/server db:generate", + "db:studio": "bun run --filter @browseros/server db:studio", + "db:push": "bun run --filter @browseros/server db:push", "lint": "bunx biome check", "lint:fix": "bunx biome check --write --unsafe", "clean": "rimraf dist" From 7bc7654c8c20c40c9ce78d3b8b4c9620c2f4e116 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Wed, 14 Jan 2026 17:33:55 -0800 Subject: [PATCH 5/5] feat: rengereate schema with new sync script --- apps/server/drizzle/.synced.json | 2 +- .../server/drizzle/0000_messy_rafael_vega.sql | 31 ---- apps/server/drizzle/meta/0000_snapshot.json | 168 ++++++++++-------- apps/server/drizzle/meta/_journal.json | 4 +- apps/server/src/lib/db/migrations/versions.ts | 93 ++++------ .../server/src/lib/db/schema/conversations.ts | 58 ++++-- apps/server/src/lib/db/schema/identity.ts | 22 ++- apps/server/src/lib/db/schema/rate-limiter.ts | 23 ++- 8 files changed, 209 insertions(+), 192 deletions(-) delete mode 100644 apps/server/drizzle/0000_messy_rafael_vega.sql diff --git a/apps/server/drizzle/.synced.json b/apps/server/drizzle/.synced.json index f05a9b69..7f1f88ea 100644 --- a/apps/server/drizzle/.synced.json +++ b/apps/server/drizzle/.synced.json @@ -1,3 +1,3 @@ { - "tags": ["0000_messy_rafael_vega"] + "tags": ["0000_living_deathbird"] } diff --git a/apps/server/drizzle/0000_messy_rafael_vega.sql b/apps/server/drizzle/0000_messy_rafael_vega.sql deleted file mode 100644 index 99a57eca..00000000 --- a/apps/server/drizzle/0000_messy_rafael_vega.sql +++ /dev/null @@ -1,31 +0,0 @@ -CREATE TABLE `identity` ( - `id` integer PRIMARY KEY NOT NULL, - `browseros_id` text NOT NULL, - `created_at` text DEFAULT (datetime('now')) NOT NULL -); ---> statement-breakpoint -CREATE TABLE `rate_limiter` ( - `id` text PRIMARY KEY NOT NULL, - `browseros_id` text NOT NULL, - `provider` text NOT NULL, - `created_at` text DEFAULT (datetime('now')) NOT NULL -); ---> statement-breakpoint -CREATE TABLE `conversations` ( - `id` text PRIMARY KEY NOT NULL, - `browseros_id` text NOT NULL, - `provider` text NOT NULL, - `model` text, - `title` text, - `created_at` text DEFAULT (datetime('now')) NOT NULL, - `updated_at` text DEFAULT (datetime('now')) NOT NULL -); ---> statement-breakpoint -CREATE TABLE `messages` ( - `id` text PRIMARY KEY NOT NULL, - `conversation_id` text NOT NULL, - `role` text NOT NULL, - `content` text NOT NULL, - `created_at` text DEFAULT (datetime('now')) NOT NULL, - FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE no action ON DELETE cascade -); diff --git a/apps/server/drizzle/meta/0000_snapshot.json b/apps/server/drizzle/meta/0000_snapshot.json index 76896225..1b6212a8 100644 --- a/apps/server/drizzle/meta/0000_snapshot.json +++ b/apps/server/drizzle/meta/0000_snapshot.json @@ -1,15 +1,15 @@ { "version": "6", "dialect": "sqlite", - "id": "1476beea-4c2e-41e4-8be4-3a5e1b7a9a6e", + "id": "070decca-6dde-4038-8921-dbe4ef14156e", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { - "identity": { - "name": "identity", + "conversations": { + "name": "conversations", "columns": { "id": { "name": "id", - "type": "integer", + "type": "text", "primaryKey": true, "notNull": true, "autoincrement": false @@ -21,6 +21,27 @@ "notNull": true, "autoincrement": false }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "text", @@ -28,16 +49,30 @@ "notNull": true, "autoincrement": false, "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_conversations_browseros_id": { + "name": "idx_conversations_browseros_id", + "columns": ["browseros_id"], + "isUnique": false } }, - "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, - "rate_limiter": { - "name": "rate_limiter", + "messages": { + "name": "messages", "columns": { "id": { "name": "id", @@ -46,15 +81,22 @@ "notNull": true, "autoincrement": false }, - "browseros_id": { - "name": "browseros_id", + "conversation_id": { + "name": "conversation_id", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, - "provider": { - "name": "provider", + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", "type": "text", "primaryKey": false, "notNull": true, @@ -69,18 +111,39 @@ "default": "(datetime('now'))" } }, - "indexes": {}, - "foreignKeys": {}, + "indexes": { + "idx_messages_conversation_id": { + "name": "idx_messages_conversation_id", + "columns": ["conversation_id"], + "isUnique": false + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraints": { + "role_check": { + "name": "role_check", + "value": "\"messages\".\"role\" IN ('user', 'assistant', 'system')" + } + } }, - "conversations": { - "name": "conversations", + "identity": { + "name": "identity", "columns": { "id": { "name": "id", - "type": "text", + "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": false @@ -92,27 +155,6 @@ "notNull": true, "autoincrement": false }, - "provider": { - "name": "provider", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "model": { - "name": "model", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, "created_at": { "name": "created_at", "type": "text", @@ -120,24 +162,21 @@ "notNull": true, "autoincrement": false, "default": "(datetime('now'))" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(datetime('now'))" } }, "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraints": { + "singleton_check": { + "name": "singleton_check", + "value": "\"identity\".\"id\" = 1" + } + } }, - "messages": { - "name": "messages", + "rate_limiter": { + "name": "rate_limiter", "columns": { "id": { "name": "id", @@ -146,22 +185,15 @@ "notNull": true, "autoincrement": false }, - "conversation_id": { - "name": "conversation_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", + "browseros_id": { + "name": "browseros_id", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, - "content": { - "name": "content", + "provider": { + "name": "provider", "type": "text", "primaryKey": false, "notNull": true, @@ -176,18 +208,14 @@ "default": "(datetime('now'))" } }, - "indexes": {}, - "foreignKeys": { - "messages_conversation_id_conversations_id_fk": { - "name": "messages_conversation_id_conversations_id_fk", - "tableFrom": "messages", - "tableTo": "conversations", - "columnsFrom": ["conversation_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" + "indexes": { + "idx_rate_limiter_browseros_id_date": { + "name": "idx_rate_limiter_browseros_id_date", + "columns": ["browseros_id", "created_at"], + "isUnique": false } }, + "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index ad1cbe7b..66ed270c 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1768436911212, - "tag": "0000_messy_rafael_vega", + "when": 1768440681533, + "tag": "0000_living_deathbird", "breakpoints": true } ] diff --git a/apps/server/src/lib/db/migrations/versions.ts b/apps/server/src/lib/db/migrations/versions.ts index 247a4249..d1ae7f77 100644 --- a/apps/server/src/lib/db/migrations/versions.ts +++ b/apps/server/src/lib/db/migrations/versions.ts @@ -12,61 +12,46 @@ export interface Migration { export const migrations: Migration[] = [ { - version: 1736864400, - name: 'initial_schema', + version: 1768440691, + name: '0000_living_deathbird', up: ` - -- Identity table (single row for this installation) - CREATE TABLE IF NOT EXISTS identity ( - id INTEGER PRIMARY KEY CHECK (id = 1), - browseros_id TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Rate limiter table (conversation_id as PK for deduplication) - CREATE TABLE IF NOT EXISTS rate_limiter ( - id TEXT PRIMARY KEY, - browseros_id TEXT NOT NULL, - provider TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Migrations tracking table - CREATE TABLE IF NOT EXISTS _migrations ( - version INTEGER PRIMARY KEY, - name TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - `, - }, - { - version: 1736950800, - name: 'conversations', - up: ` - -- Conversations table - CREATE TABLE IF NOT EXISTS conversations ( - id TEXT PRIMARY KEY, - browseros_id TEXT NOT NULL, - provider TEXT NOT NULL, - model TEXT, - title TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Messages table - CREATE TABLE IF NOT EXISTS messages ( - id TEXT PRIMARY KEY, - conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, - role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), - content TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Indexes for common queries - CREATE INDEX IF NOT EXISTS idx_conversations_browseros_id ON conversations(browseros_id); - CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); - CREATE INDEX IF NOT EXISTS idx_rate_limiter_browseros_id_date - ON rate_limiter(browseros_id, created_at); +CREATE TABLE \`conversations\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`browseros_id\` text NOT NULL, + \`provider\` text NOT NULL, + \`model\` text, + \`title\` text, + \`created_at\` text DEFAULT (datetime('now')) NOT NULL, + \`updated_at\` text DEFAULT (datetime('now')) NOT NULL +); +--> statement-breakpoint +CREATE INDEX \`idx_conversations_browseros_id\` ON \`conversations\` (\`browseros_id\`);--> statement-breakpoint +CREATE TABLE \`messages\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`conversation_id\` text NOT NULL, + \`role\` text NOT NULL, + \`content\` text NOT NULL, + \`created_at\` text DEFAULT (datetime('now')) NOT NULL, + FOREIGN KEY (\`conversation_id\`) REFERENCES \`conversations\`(\`id\`) ON UPDATE no action ON DELETE cascade, + CONSTRAINT "role_check" CHECK("messages"."role" IN ('user', 'assistant', 'system')) +); +--> statement-breakpoint +CREATE INDEX \`idx_messages_conversation_id\` ON \`messages\` (\`conversation_id\`);--> statement-breakpoint +CREATE TABLE \`identity\` ( + \`id\` integer PRIMARY KEY NOT NULL, + \`browseros_id\` text NOT NULL, + \`created_at\` text DEFAULT (datetime('now')) NOT NULL, + CONSTRAINT "singleton_check" CHECK("identity"."id" = 1) +); +--> statement-breakpoint +CREATE TABLE \`rate_limiter\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`browseros_id\` text NOT NULL, + \`provider\` text NOT NULL, + \`created_at\` text DEFAULT (datetime('now')) NOT NULL +); +--> statement-breakpoint +CREATE INDEX \`idx_rate_limiter_browseros_id_date\` ON \`rate_limiter\` (\`browseros_id\`,\`created_at\`); `, }, ] diff --git a/apps/server/src/lib/db/schema/conversations.ts b/apps/server/src/lib/db/schema/conversations.ts index 10eab8ec..ed99781a 100644 --- a/apps/server/src/lib/db/schema/conversations.ts +++ b/apps/server/src/lib/db/schema/conversations.ts @@ -4,24 +4,44 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { sql } from 'drizzle-orm' -import { sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { check, index, sqliteTable, text } from 'drizzle-orm/sqlite-core' -export const conversations = sqliteTable('conversations', { - id: text('id').primaryKey(), - browserosId: text('browseros_id').notNull(), - provider: text('provider').notNull(), - model: text('model'), - title: text('title'), - createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), - updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`), -}) +export const conversations = sqliteTable( + 'conversations', + { + id: text('id').primaryKey(), + browserosId: text('browseros_id').notNull(), + provider: text('provider').notNull(), + model: text('model'), + title: text('title'), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), + updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`), + }, + (table) => ({ + browserosIdIdx: index('idx_conversations_browseros_id').on( + table.browserosId, + ), + }), +) -export const messages = sqliteTable('messages', { - id: text('id').primaryKey(), - conversationId: text('conversation_id') - .notNull() - .references(() => conversations.id, { onDelete: 'cascade' }), - role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(), - content: text('content').notNull(), - createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), -}) +export const messages = sqliteTable( + 'messages', + { + id: text('id').primaryKey(), + conversationId: text('conversation_id') + .notNull() + .references(() => conversations.id, { onDelete: 'cascade' }), + role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(), + content: text('content').notNull(), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), + }, + (table) => ({ + conversationIdIdx: index('idx_messages_conversation_id').on( + table.conversationId, + ), + roleCheck: check( + 'role_check', + sql`${table.role} IN ('user', 'assistant', 'system')`, + ), + }), +) diff --git a/apps/server/src/lib/db/schema/identity.ts b/apps/server/src/lib/db/schema/identity.ts index c52aaf5a..cb13ea14 100644 --- a/apps/server/src/lib/db/schema/identity.ts +++ b/apps/server/src/lib/db/schema/identity.ts @@ -4,12 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { sql } from 'drizzle-orm' -import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { check, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' -export const identity = sqliteTable('identity', { - id: integer('id') - .primaryKey() - .$default(() => 1), - browserosId: text('browseros_id').notNull(), - createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), -}) +export const identity = sqliteTable( + 'identity', + { + id: integer('id') + .primaryKey() + .$default(() => 1), + browserosId: text('browseros_id').notNull(), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), + }, + (table) => ({ + singletonCheck: check('singleton_check', sql`${table.id} = 1`), + }), +) diff --git a/apps/server/src/lib/db/schema/rate-limiter.ts b/apps/server/src/lib/db/schema/rate-limiter.ts index f0b8bc05..41ad0d13 100644 --- a/apps/server/src/lib/db/schema/rate-limiter.ts +++ b/apps/server/src/lib/db/schema/rate-limiter.ts @@ -4,11 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { sql } from 'drizzle-orm' -import { sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core' -export const rateLimiter = sqliteTable('rate_limiter', { - id: text('id').primaryKey(), - browserosId: text('browseros_id').notNull(), - provider: text('provider').notNull(), - createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), -}) +export const rateLimiter = sqliteTable( + 'rate_limiter', + { + id: text('id').primaryKey(), + browserosId: text('browseros_id').notNull(), + provider: text('provider').notNull(), + createdAt: text('created_at').notNull().default(sql`(datetime('now'))`), + }, + (table) => ({ + browserosIdDateIdx: index('idx_rate_limiter_browseros_id_date').on( + table.browserosId, + table.createdAt, + ), + }), +)