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>
This commit is contained in:
126
src/relay/db/memory.ts
Normal file
126
src/relay/db/memory.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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 */ },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user