Files
CookieBridge/src/relay/db/memory.ts
徐枫 1093d64724 feat: add SQLite and MySQL database support with setup wizard selection (RCA-21)
Replace in-memory storage with a database abstraction layer supporting SQLite
and MySQL. Users choose their preferred database during the first-time setup
wizard. The server persists the database config to data/db-config.json and
loads it automatically on restart.

- Add database abstraction interfaces (ICookieStore, IDeviceStore, IAgentStore, IAdminStore)
- Implement SQLite driver using better-sqlite3 with WAL mode
- Implement MySQL driver using mysql2 connection pooling
- Keep memory-backed driver for backwards compatibility and testing
- Add database selection step (step 2) to the setup wizard UI
- Update setup API to accept dbConfig and initialize the chosen database
- Update RelayServer to use async store interfaces with runtime store replacement

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 11:55:59 +08:00

127 lines
4.3 KiB
TypeScript

import { CookieBlobStore } from "../store.js";
import { DeviceRegistry, AgentRegistry } from "../tokens.js";
import { AdminStore } from "../admin/auth.js";
import type {
ICookieStore,
IDeviceStore,
IAgentStore,
IAdminStore,
DataStores,
} from "./types.js";
import type { EncryptedCookieBlob, DeviceInfo, AgentToken } from "../../protocol/spec.js";
import type { AdminSettings } from "../admin/auth.js";
class MemoryCookieStore implements ICookieStore {
private inner = new CookieBlobStore();
async upsert(blob: Omit<EncryptedCookieBlob, "id" | "updatedAt">): Promise<EncryptedCookieBlob> {
return this.inner.upsert(blob);
}
async delete(deviceId: string, domain: string, cookieName: string, path: string): Promise<boolean> {
return this.inner.delete(deviceId, domain, cookieName, path);
}
async deleteById(id: string): Promise<boolean> {
return this.inner.deleteById(id);
}
async getByDevice(deviceId: string, domain?: string): Promise<EncryptedCookieBlob[]> {
return this.inner.getByDevice(deviceId, domain);
}
async getByDevices(deviceIds: string[], domain?: string): Promise<EncryptedCookieBlob[]> {
return this.inner.getByDevices(deviceIds, domain);
}
async getAll(): Promise<EncryptedCookieBlob[]> {
return this.inner.getAll();
}
async getById(id: string): Promise<EncryptedCookieBlob | null> {
return this.inner.getById(id);
}
async getUpdatedSince(deviceIds: string[], since: string): Promise<EncryptedCookieBlob[]> {
return this.inner.getUpdatedSince(deviceIds, since);
}
}
class MemoryDeviceStore implements IDeviceStore {
private inner = new DeviceRegistry();
async register(deviceId: string, name: string, platform: string, encPub: string): Promise<DeviceInfo> {
return this.inner.register(deviceId, name, platform, encPub);
}
async getByToken(token: string): Promise<DeviceInfo | null> {
return this.inner.getByToken(token);
}
async getById(deviceId: string): Promise<DeviceInfo | null> {
return this.inner.getById(deviceId);
}
async addPairing(deviceIdA: string, deviceIdB: string): Promise<void> {
this.inner.addPairing(deviceIdA, deviceIdB);
}
async getPairedDevices(deviceId: string): Promise<string[]> {
return this.inner.getPairedDevices(deviceId);
}
async getPairingGroup(deviceId: string): Promise<string[]> {
return this.inner.getPairingGroup(deviceId);
}
async listAll(): Promise<DeviceInfo[]> {
return this.inner.listAll();
}
async revoke(deviceId: string): Promise<boolean> {
return this.inner.revoke(deviceId);
}
}
class MemoryAgentStore implements IAgentStore {
private inner = new AgentRegistry();
async create(name: string, encPub: string, allowedDomains: string[]): Promise<AgentToken> {
return this.inner.create(name, encPub, allowedDomains);
}
async getByToken(token: string): Promise<AgentToken | null> {
return this.inner.getByToken(token);
}
async grantAccess(agentId: string, deviceId: string): Promise<void> {
this.inner.grantAccess(agentId, deviceId);
}
async getAccessibleDevices(agentId: string): Promise<string[]> {
return this.inner.getAccessibleDevices(agentId);
}
async revokeAccess(agentId: string, deviceId: string): Promise<void> {
this.inner.revokeAccess(agentId, deviceId);
}
}
class MemoryAdminStore implements IAdminStore {
private inner = new AdminStore();
get isSetUp(): boolean {
return this.inner.isSetUp;
}
async setup(username: string, password: string): Promise<void> {
return this.inner.setup(username, password);
}
async login(username: string, password: string): Promise<string> {
return this.inner.login(username, password);
}
verifyToken(token: string): { sub: string; role: string } {
return this.inner.verifyToken(token);
}
getUser(): { username: string; createdAt: string } | null {
return this.inner.getUser();
}
getSettings(): AdminSettings {
return this.inner.getSettings();
}
updateSettings(patch: Partial<AdminSettings>): AdminSettings {
return this.inner.updateSettings(patch);
}
}
export function createMemoryStores(): DataStores {
return {
cookieStore: new MemoryCookieStore(),
deviceStore: new MemoryDeviceStore(),
agentStore: new MemoryAgentStore(),
adminStore: new MemoryAdminStore(),
async close() { /* no-op */ },
};
}