2026-03-05 00:04:26 -06:00
import Database from "bun:sqlite" ;
import { ClientInfo , ListFilters , ListResult , ClientRole } from "./types" ;
import { getThumbnail } from "./thumbnails" ;
import { resolve } from "path" ;
import { ensureDataDir } from "./paths" ;
const dataDir = ensureDataDir ();
const dbPath = resolve ( dataDir , "overlord.db" );
const db = new Database ( dbPath );
console . log ( `[db] Using database at: ${ dbPath } ` );
db . run ( `
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
hwid TEXT,
role TEXT,
ip TEXT,
host TEXT,
os TEXT,
arch TEXT,
version TEXT,
user TEXT,
2026-03-10 19:30:51 -05:00
nickname TEXT,
2026-03-13 23:46:42 -05:00
custom_tag TEXT,
custom_tag_note TEXT,
2026-03-05 00:04:26 -06:00
monitors INTEGER,
country TEXT,
last_seen INTEGER,
online INTEGER,
2026-03-17 00:11:25 -05:00
ping_ms INTEGER,
2026-04-13 22:45:12 -05:00
bookmarked INTEGER NOT NULL DEFAULT 0,
build_tag TEXT,
built_by_user_id INTEGER
2026-03-05 00:04:26 -06:00
);
` );
try {
db . run ( `ALTER TABLE clients ADD COLUMN role TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN hwid TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN ip TEXT` );
} catch {}
2026-03-10 19:30:51 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN nickname TEXT` );
} catch {}
2026-03-13 23:46:42 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN custom_tag TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN custom_tag_note TEXT` );
} catch {}
2026-03-17 00:11:25 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN bookmarked INTEGER NOT NULL DEFAULT 0` );
} catch {}
2026-04-13 22:45:12 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN build_tag TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN built_by_user_id INTEGER` );
} catch {}
2026-03-20 22:38:17 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN enrollment_status TEXT NOT NULL DEFAULT 'pending'` );
} catch {}
2026-04-04 23:20:26 -05:00
try {
db . run ( `UPDATE clients SET enrollment_status='pending' WHERE enrollment_status IS NULL` );
} catch {}
2026-03-20 22:38:17 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN public_key TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN key_fingerprint TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN enrolled_at INTEGER` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN enrolled_by TEXT` );
} catch {}
2026-04-01 17:27:31 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN cpu TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN gpu TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN ram TEXT` );
} catch {}
2026-04-07 10:36:29 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0` );
} catch {}
2026-03-20 22:38:17 -05:00
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_public_key ON clients(public_key);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_key_fingerprint ON clients(key_fingerprint);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_enrollment_status ON clients(enrollment_status);` ,
);
2026-03-09 22:43:33 -05:00
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_os_last_seen ON clients(os, last_seen DESC);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_ping_ms ON clients(ping_ms);` ,
);
2026-04-13 22:45:12 -05:00
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_built_by_user_id ON clients(built_by_user_id);` ,
);
2026-04-05 22:55:03 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN disconnect_reason TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN disconnect_detail TEXT` );
} catch {}
2026-04-08 20:59:46 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN elevation TEXT` );
} catch {}
2026-04-13 10:49:36 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN permissions TEXT` );
} catch {}
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS banned_ips (
ip TEXT PRIMARY KEY,
reason TEXT,
created_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_banned_ips_created_at ON banned_ips(created_at DESC);` ,
);
2026-03-21 17:03:42 -05:00
db . run ( `
CREATE TABLE IF NOT EXISTS revoked_tokens (
token_hash TEXT PRIMARY KEY,
expires_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_revoked_tokens_expires ON revoked_tokens(expires_at);` ,
);
2026-04-13 17:15:06 -05:00
db . run ( `
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL,
ip TEXT,
user_agent TEXT,
created_at INTEGER NOT NULL,
last_activity INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
` );
db . run ( `CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);` );
db . run ( `CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);` );
db . run ( `CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);` );
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS builds (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
start_time INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
2026-03-12 21:26:53 -05:00
files TEXT NOT NULL,
build_tag TEXT,
built_by_user_id INTEGER
2026-03-05 00:04:26 -06:00
);
` );
2026-03-12 21:26:53 -05:00
try { db . run ( `ALTER TABLE builds ADD COLUMN build_tag TEXT` ); } catch {}
try { db . run ( `ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER` ); } catch {}
db . run ( `CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);` );
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS notification_screenshots (
id TEXT PRIMARY KEY,
notification_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
format TEXT NOT NULL,
width INTEGER,
height INTEGER,
bytes BLOB NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_notification_id ON notification_screenshots(notification_id);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_ts ON notification_screenshots(ts DESC);` ,
);
2026-03-23 20:24:35 +01:00
db . run ( `
CREATE TABLE IF NOT EXISTS push_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
created_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id);` ,
);
db . run (
`CREATE UNIQUE INDEX IF NOT EXISTS idx_push_subscriptions_endpoint ON push_subscriptions(endpoint);` ,
);
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS auto_scripts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
trigger TEXT NOT NULL,
script TEXT NOT NULL,
script_type TEXT NOT NULL,
enabled INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_auto_scripts_trigger ON auto_scripts(trigger, enabled);` ,
);
2026-03-17 13:27:44 -05:00
try {
db . run ( `ALTER TABLE auto_scripts ADD COLUMN os_filter TEXT NOT NULL DEFAULT '[]'` );
} catch { /* column already exists */ }
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS auto_script_runs (
script_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
PRIMARY KEY (script_id, client_id)
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_auto_script_runs_ts ON auto_script_runs(ts DESC);` ,
);
2026-03-29 17:53:25 -05:00
db . run ( `
CREATE TABLE IF NOT EXISTS auto_deploys (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
trigger TEXT NOT NULL,
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
file_os TEXT NOT NULL,
args TEXT NOT NULL DEFAULT '',
hide_window INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL,
os_filter TEXT NOT NULL DEFAULT '[]',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_auto_deploys_trigger ON auto_deploys(trigger, enabled);` ,
);
db . run ( `
CREATE TABLE IF NOT EXISTS auto_deploy_runs (
deploy_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
PRIMARY KEY (deploy_id, client_id)
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_auto_deploy_runs_ts ON auto_deploy_runs(ts DESC);` ,
);
2026-04-08 10:33:22 -05:00
db . run ( `
CREATE TABLE IF NOT EXISTS shared_files (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
stored_path TEXT NOT NULL,
size INTEGER NOT NULL,
mime_type TEXT NOT NULL,
uploaded_by INTEGER NOT NULL,
uploaded_by_username TEXT NOT NULL,
password_hash TEXT,
max_downloads INTEGER,
download_count INTEGER NOT NULL DEFAULT 0,
expires_at INTEGER,
created_at INTEGER NOT NULL,
description TEXT
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_shared_files_created_at ON shared_files(created_at DESC);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_shared_files_uploaded_by ON shared_files(uploaded_by);` ,
);
export type SharedFileRecord = {
id : string ;
filename : string ;
storedPath : string ;
size : number ;
mimeType : string ;
uploadedBy : number ;
uploadedByUsername : string ;
passwordHash : string | null ;
maxDownloads : number | null ;
downloadCount : number ;
expiresAt : number | null ;
createdAt : number ;
description : string | null ;
};
export function insertSharedFile ( file : SharedFileRecord ) : void {
db . run (
`INSERT INTO shared_files (id, filename, stored_path, size, mime_type, uploaded_by, uploaded_by_username, password_hash, max_downloads, download_count, expires_at, created_at, description)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
file . id ,
file . filename ,
file . storedPath ,
file . size ,
file . mimeType ,
file . uploadedBy ,
file . uploadedByUsername ,
file . passwordHash ,
file . maxDownloads ,
file . downloadCount ,
file . expiresAt ,
file . createdAt ,
file . description ,
);
}
export function getSharedFile ( id : string ) : SharedFileRecord | null {
const row = db . query < any >( `SELECT * FROM shared_files WHERE id=?` ). get ( id );
if ( ! row ) return null ;
return {
id : row.id ,
filename : row.filename ,
storedPath : row.stored_path ,
size : row.size ,
mimeType : row.mime_type ,
uploadedBy : row.uploaded_by ,
uploadedByUsername : row.uploaded_by_username ,
passwordHash : row.password_hash ,
maxDownloads : row.max_downloads ,
downloadCount : row.download_count ,
expiresAt : row.expires_at ,
createdAt : row.created_at ,
description : row.description ,
};
}
export function listSharedFiles () : SharedFileRecord [] {
return db
. query < any >( `SELECT * FROM shared_files ORDER BY created_at DESC` )
. all ()
. map (( row : any ) => ({
id : row.id ,
filename : row.filename ,
storedPath : row.stored_path ,
size : row.size ,
mimeType : row.mime_type ,
uploadedBy : row.uploaded_by ,
uploadedByUsername : row.uploaded_by_username ,
passwordHash : row.password_hash ,
maxDownloads : row.max_downloads ,
downloadCount : row.download_count ,
expiresAt : row.expires_at ,
createdAt : row.created_at ,
description : row.description ,
}));
}
export function deleteSharedFile ( id : string ) : boolean {
const result = db . run ( `DELETE FROM shared_files WHERE id=?` , id );
return ( result as any ) ? . changes ? ( result as any ). changes > 0 : false ;
}
export function updateSharedFile (
id : string ,
updates : {
passwordHash? : string | null ;
maxDownloads? : number | null ;
expiresAt? : number | null ;
description? : string | null ;
},
) : boolean {
const setClauses : string [] = [];
const values : any [] = [];
if ( updates . passwordHash !== undefined ) {
setClauses . push ( "password_hash=?" );
values . push ( updates . passwordHash );
}
if ( updates . maxDownloads !== undefined ) {
setClauses . push ( "max_downloads=?" );
values . push ( updates . maxDownloads );
}
if ( updates . expiresAt !== undefined ) {
setClauses . push ( "expires_at=?" );
values . push ( updates . expiresAt );
}
if ( updates . description !== undefined ) {
setClauses . push ( "description=?" );
values . push ( updates . description );
}
if ( setClauses . length === 0 ) return false ;
values . push ( id );
const result = db . run (
`UPDATE shared_files SET ${ setClauses . join ( ", " ) } WHERE id=?` ,
... values ,
);
return ( result as any ) ? . changes ? ( result as any ). changes > 0 : false ;
}
export function incrementSharedFileDownloadCount ( id : string ) : boolean {
const result = db . run (
`UPDATE shared_files SET download_count = download_count + 1 WHERE id=?` ,
id ,
);
return ( result as any ) ? . changes ? ( result as any ). changes > 0 : false ;
}
export function deleteExpiredSharedFiles () : string [] {
const now = Date . now ();
const expired = db
. query < any >( `SELECT id, stored_path FROM shared_files WHERE expires_at IS NOT NULL AND expires_at < ?` )
. all ( now );
const ids = expired . map (( r : any ) => r . id );
const paths = expired . map (( r : any ) => r . stored_path );
if ( ids . length > 0 ) {
for ( const id of ids ) {
db . run ( `DELETE FROM shared_files WHERE id=?` , id );
}
}
return paths ;
}
2026-03-05 00:04:26 -06:00
export function upsertClientRow (
partial : Partial < ClientInfo > & {
id : string ;
lastSeen? : number ;
online? : number ;
},
) {
const now = partial . lastSeen ?? Date . now ();
db . run (
2026-04-13 22:45:12 -05:00
`INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, build_tag, built_by_user_id, enrollment_status, public_key, key_fingerprint, cpu, gpu, ram, is_admin, elevation, permissions)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, 0), ?, ?)
2026-03-05 00:04:26 -06:00
ON CONFLICT(id) DO UPDATE SET
hwid=COALESCE(excluded.hwid, clients.hwid),
role=COALESCE(excluded.role, clients.role),
ip=COALESCE(excluded.ip, clients.ip),
host=COALESCE(excluded.host, clients.host),
os=COALESCE(excluded.os, clients.os),
arch=COALESCE(excluded.arch, clients.arch),
version=COALESCE(excluded.version, clients.version),
user=COALESCE(excluded.user, clients.user),
2026-03-10 19:30:51 -05:00
nickname=clients.nickname,
2026-03-13 23:46:42 -05:00
custom_tag=clients.custom_tag,
custom_tag_note=clients.custom_tag_note,
2026-03-05 00:04:26 -06:00
monitors=COALESCE(excluded.monitors, clients.monitors),
country=COALESCE(excluded.country, clients.country),
last_seen=excluded.last_seen,
online=COALESCE(excluded.online, clients.online),
2026-03-20 22:38:17 -05:00
ping_ms=COALESCE(excluded.ping_ms, clients.ping_ms),
2026-04-13 22:45:12 -05:00
build_tag=COALESCE(excluded.build_tag, clients.build_tag),
built_by_user_id=COALESCE(excluded.built_by_user_id, clients.built_by_user_id),
2026-03-29 17:53:25 -05:00
enrollment_status=CASE WHEN excluded.enrollment_status <> 'pending' THEN excluded.enrollment_status ELSE clients.enrollment_status END,
2026-03-20 22:38:17 -05:00
public_key=COALESCE(excluded.public_key, clients.public_key),
2026-04-01 17:27:31 -05:00
key_fingerprint=COALESCE(excluded.key_fingerprint, clients.key_fingerprint),
cpu=COALESCE(excluded.cpu, clients.cpu),
gpu=COALESCE(excluded.gpu, clients.gpu),
2026-04-07 10:36:29 -05:00
ram=COALESCE(excluded.ram, clients.ram),
2026-04-08 20:59:46 -05:00
is_admin=COALESCE(excluded.is_admin, clients.is_admin),
2026-04-13 10:49:36 -05:00
elevation=COALESCE(excluded.elevation, clients.elevation),
permissions=COALESCE(excluded.permissions, clients.permissions)
2026-03-05 00:04:26 -06:00
` ,
partial . id ,
partial . hwid ?? partial . id ,
partial . role ?? null ,
partial . ip ?? null ,
partial . host ?? null ,
partial . os ?? null ,
partial . arch ?? null ,
partial . version ?? null ,
partial . user ?? null ,
2026-03-13 23:46:42 -05:00
partial . nickname ?? null ,
partial . customTag ?? null ,
partial . customTagNote ?? null ,
2026-03-05 00:04:26 -06:00
partial . monitors ?? null ,
partial . country ?? null ,
now ,
partial . online ?? 0 ,
partial . pingMs ?? null ,
2026-04-13 22:45:12 -05:00
partial . buildTag ?? null ,
partial . builtByUserId ?? null ,
2026-03-20 22:38:17 -05:00
partial . enrollmentStatus ?? "pending" ,
partial . publicKey ?? null ,
partial . keyFingerprint ?? null ,
2026-04-01 17:27:31 -05:00
partial . cpu ?? null ,
partial . gpu ?? null ,
partial . ram ?? null ,
2026-04-07 10:36:29 -05:00
partial . isAdmin !== undefined ? ( partial . isAdmin ? 1 : 0 ) : null ,
2026-04-08 20:59:46 -05:00
partial . elevation ?? null ,
2026-04-13 10:49:36 -05:00
partial . permissions ? JSON . stringify ( partial . permissions ) : null ,
2026-03-05 00:04:26 -06:00
);
if ( partial . hwid ) {
db . run (
`DELETE FROM clients WHERE hwid=? AND id<>?` ,
partial . hwid ,
partial . id ,
);
}
}
2026-04-05 22:55:03 -05:00
export function setOnlineState ( id : string , online : boolean , disconnectReason? : string , disconnectDetail? : string ) {
if ( online ) {
db . run (
`UPDATE clients SET online=1, last_seen=?, disconnect_reason=NULL, disconnect_detail=NULL WHERE id=?` ,
Date . now (),
id ,
);
} else {
db . run (
`UPDATE clients SET online=0, last_seen=?, disconnect_reason=?, disconnect_detail=? WHERE id=?` ,
Date . now (),
disconnectReason ?? null ,
disconnectDetail ?? null ,
id ,
);
}
2026-03-05 00:04:26 -06:00
}
export function deleteClientRow ( id : string ) {
db . run ( `DELETE FROM clients WHERE id=?` , id );
}
2026-03-28 22:18:33 -05:00
export function deleteOfflineClientRows () : number {
const result = db . run ( `DELETE FROM clients WHERE online=0` );
return ( result as any ) ? . changes || 0 ;
}
2026-03-10 19:30:51 -05:00
export function getClientOnlineState ( id : string ) : boolean | null {
const row = db . query < { online : number } > ( `SELECT online FROM clients WHERE id=?` ). get ( id );
if ( ! row ) return null ;
return row . online === 1 ;
}
export function setClientNickname ( id : string , nickname : string | null ) : boolean {
const result = db . run (
`UPDATE clients SET nickname=? WHERE id=?` ,
nickname && nickname . trim () ? nickname . trim () : null ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
export function getClientNickname ( id : string ) : string | null {
const row = db . query < { nickname : string | null } > ( `SELECT nickname FROM clients WHERE id=?` ). get ( id );
return row ? . nickname ?? null ;
}
2026-03-13 23:46:42 -05:00
export function setClientTag (
id : string ,
tag : string | null ,
note : string | null ,
) : boolean {
const normalizedTag = tag && tag . trim () ? tag . trim () : null ;
const normalizedNote = normalizedTag ? note ?? null : null ;
const result = db . run (
`UPDATE clients SET custom_tag=?, custom_tag_note=? WHERE id=?` ,
normalizedTag ,
normalizedNote ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
2026-03-05 00:04:26 -06:00
export function getClientIp ( id : string ) : string | null {
const row = db . query < { ip : string } > ( `SELECT ip FROM clients WHERE id=?` ). get ( id );
return row ? . ip || null ;
}
export function banIp ( ip : string , reason? : string ) {
db . run (
`INSERT OR REPLACE INTO banned_ips (ip, reason, created_at) VALUES (?, ?, ?)`
, ip ,
reason || null ,
Date . now (),
);
}
export function unbanIp ( ip : string ) {
db . run ( `DELETE FROM banned_ips WHERE ip=?` , ip );
}
2026-03-06 20:50:02 -06:00
export type BannedIpEntry = {
ip : string ;
reason : string | null ;
createdAt : number ;
};
export function listBannedIps () : BannedIpEntry [] {
const rows = db
. query < { ip : string ; reason : string | null ; createdAt : number } > (
`SELECT ip, reason, created_at as createdAt FROM banned_ips ORDER BY created_at DESC` ,
)
. all ();
return rows . map (( row ) => ({
ip : row.ip ,
reason : row.reason ,
createdAt : Number ( row . createdAt ) || 0 ,
}));
}
2026-03-05 00:04:26 -06:00
export function isIpBanned ( ip : string ) : boolean {
const row = db . query < { ip : string } > ( `SELECT ip FROM banned_ips WHERE ip=?` ). get ( ip );
return !! row ? . ip ;
}
2026-03-21 17:03:42 -05:00
// ── Revoked token persistence ──
function hashToken ( token : string ) : string {
return new Bun . CryptoHasher ( "sha256" ). update ( token ). digest ( "hex" );
}
export function persistRevokedToken ( token : string , expiresAt : number ) : void {
db . run (
`INSERT OR IGNORE INTO revoked_tokens (token_hash, expires_at) VALUES (?, ?)` ,
hashToken ( token ),
expiresAt ,
);
}
2026-04-13 17:15:06 -05:00
export function persistRevokedTokenHash ( tokenHash : string , expiresAt : number ) : void {
db . run (
`INSERT OR IGNORE INTO revoked_tokens (token_hash, expires_at) VALUES (?, ?)` ,
tokenHash ,
expiresAt ,
);
}
2026-03-21 17:03:42 -05:00
export function isTokenRevoked ( token : string ) : boolean {
const row = db . query < { token_hash : string } > (
`SELECT token_hash FROM revoked_tokens WHERE token_hash=?` ,
). get ( hashToken ( token ));
return !! row ;
}
export function loadAllRevokedTokenHashes () : Set < string > {
const now = Math . floor ( Date . now () / 1000 );
const rows = db . query < { token_hash : string } > (
`SELECT token_hash FROM revoked_tokens WHERE expires_at > ?` ,
). all ( now );
return new Set ( rows . map (( r ) => r . token_hash ));
}
export function pruneExpiredRevokedTokens () : number {
const now = Math . floor ( Date . now () / 1000 );
const result = db . run ( `DELETE FROM revoked_tokens WHERE expires_at <= ?` , now );
return result . changes ;
}
2026-04-13 17:15:06 -05:00
export type SessionRecord = {
id : string ;
userId : number ;
tokenHash : string ;
ip : string | null ;
userAgent : string | null ;
createdAt : number ;
lastActivity : number ;
expiresAt : number ;
revoked : boolean ;
};
export function createSession ( session : {
id : string ;
userId : number ;
tokenHash : string ;
ip : string | null ;
userAgent : string | null ;
createdAt : number ;
expiresAt : number ;
}) : void {
db . run (
`INSERT INTO sessions (id, user_id, token_hash, ip, user_agent, created_at, last_activity, expires_at, revoked)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)` ,
session . id ,
session . userId ,
session . tokenHash ,
session . ip ,
session . userAgent ,
session . createdAt ,
session . createdAt ,
session . expiresAt ,
);
}
export function getSessionByTokenHash ( tokenHash : string ) : SessionRecord | null {
const row = db . query < any >(
`SELECT * FROM sessions WHERE token_hash=? AND revoked=0` ,
). get ( tokenHash );
if ( ! row ) return null ;
return mapSessionRow ( row );
}
export function getSessionById ( id : string ) : SessionRecord | null {
const row = db . query < any >( `SELECT * FROM sessions WHERE id=?` ). get ( id );
if ( ! row ) return null ;
return mapSessionRow ( row );
}
export function listUserSessions ( userId : number ) : SessionRecord [] {
const rows = db . query < any >(
`SELECT * FROM sessions WHERE user_id=? ORDER BY created_at DESC` ,
). all ( userId );
return rows . map ( mapSessionRow );
}
export function updateSessionActivity ( tokenHash : string ) : void {
const now = Math . floor ( Date . now () / 1000 );
db . run ( `UPDATE sessions SET last_activity=? WHERE token_hash=? AND revoked=0` , now , tokenHash );
}
export function revokeSessionByTokenHash ( tokenHash : string ) : boolean {
const result = db . run ( `UPDATE sessions SET revoked=1 WHERE token_hash=? AND revoked=0` , tokenHash );
return result . changes > 0 ;
}
export function revokeSessionById ( sessionId : string ) : { tokenHash : string | null } {
const row = db . query < { token_hash : string } > (
`SELECT token_hash FROM sessions WHERE id=? AND revoked=0` ,
). get ( sessionId );
if ( ! row ) return { tokenHash : null };
db . run ( `UPDATE sessions SET revoked=1 WHERE id=?` , sessionId );
return { tokenHash : row.token_hash };
}
export function revokeAllUserSessions ( userId : number ) : number {
const result = db . run ( `UPDATE sessions SET revoked=1 WHERE user_id=? AND revoked=0` , userId );
return result . changes ;
}
export function pruneExpiredSessions () : number {
const now = Math . floor ( Date . now () / 1000 );
const result = db . run ( `DELETE FROM sessions WHERE expires_at <= ?` , now );
return result . changes ;
}
export function hashTokenForSession ( token : string ) : string {
return hashToken ( token );
}
function mapSessionRow ( row : any ) : SessionRecord {
return {
id : row.id ,
userId : row.user_id ,
tokenHash : row.token_hash ,
ip : row.ip ,
userAgent : row.user_agent ,
createdAt : row.created_at ,
lastActivity : row.last_activity ,
expiresAt : row.expires_at ,
revoked : !! row . revoked ,
};
}
2026-03-05 00:04:26 -06:00
export function markAllClientsOffline() {
db . run ( `UPDATE clients SET online=0` );
console . log ( "[db] marked all clients as offline" );
}
export function listClients ( filters : ListFilters ) : ListResult {
2026-03-06 20:50:02 -06:00
const {
page ,
pageSize ,
search ,
sort ,
statusFilter ,
osFilter ,
2026-03-17 00:11:25 -05:00
countryFilter ,
2026-03-20 22:38:17 -05:00
enrollmentFilter ,
2026-04-13 22:45:12 -05:00
builtByUserId ,
requireBuildOwner ,
2026-03-06 20:50:02 -06:00
allowedClientIds ,
deniedClientIds ,
} = filters ;
2026-03-05 00:04:26 -06:00
const where : string [] = [];
const params : any [] = [];
if ( search ) {
where . push (
2026-03-13 23:46:42 -05:00
"(LOWER(COALESCE(host,'')) LIKE ? OR LOWER(COALESCE(user,'')) LIKE ? OR LOWER(COALESCE(nickname,'')) LIKE ? OR LOWER(COALESCE(custom_tag,'')) LIKE ? OR LOWER(COALESCE(custom_tag_note,'')) LIKE ? OR LOWER(id) LIKE ?)" ,
2026-03-05 00:04:26 -06:00
);
const needle = `% ${ search } %` ;
2026-03-13 23:46:42 -05:00
params . push ( needle , needle , needle , needle , needle , needle );
2026-03-05 00:04:26 -06:00
}
if ( statusFilter === "online" ) {
where . push ( "online=1" );
} else if ( statusFilter === "offline" ) {
where . push ( "online=0" );
}
2026-03-20 22:38:17 -05:00
if ( enrollmentFilter && enrollmentFilter !== "all" ) {
2026-04-04 23:20:26 -05:00
if ( enrollmentFilter === "pending" ) {
where . push ( "(enrollment_status='pending' OR enrollment_status IS NULL)" );
} else {
where . push ( "enrollment_status=?" );
params . push ( enrollmentFilter );
}
2026-03-20 22:38:17 -05:00
}
2026-03-05 00:04:26 -06:00
if ( osFilter && osFilter !== "all" ) {
where . push ( "os=?" );
params . push ( osFilter );
}
2026-03-17 00:11:25 -05:00
if ( countryFilter && countryFilter !== "all" ) {
where . push ( "UPPER(COALESCE(country,'ZZ'))=?" );
params . push ( countryFilter . toUpperCase ());
}
2026-04-13 22:45:12 -05:00
if ( typeof builtByUserId === "number" ) {
where . push ( "built_by_user_id=?" );
params . push ( builtByUserId );
}
if ( requireBuildOwner ) {
where . push ( "built_by_user_id IS NOT NULL" );
}
2026-03-06 20:50:02 -06:00
if ( Array . isArray ( allowedClientIds )) {
if ( allowedClientIds . length === 0 ) {
where . push ( "1=0" );
} else {
where . push ( `id IN ( ${ allowedClientIds . map (() => "?" ). join ( "," ) } )` );
params . push (... allowedClientIds );
}
}
if ( Array . isArray ( deniedClientIds ) && deniedClientIds . length > 0 ) {
where . push ( `id NOT IN ( ${ deniedClientIds . map (() => "?" ). join ( "," ) } )` );
params . push (... deniedClientIds );
}
2026-03-05 00:04:26 -06:00
const whereSql = where . length ? `WHERE ${ where . join ( " AND " ) } ` : "" ;
const orderBy = (() => {
2026-03-22 23:21:27 -05:00
const online = "online DESC" ;
2026-03-17 00:11:25 -05:00
const bookmark = "bookmarked DESC" ;
2026-03-05 00:04:26 -06:00
switch ( sort ) {
2026-03-17 00:42:57 -05:00
case "stable" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , id ASC` ;
2026-03-05 00:04:26 -06:00
case "ping_asc" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , ping_ms IS NULL, ping_ms ASC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "ping_desc" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , ping_ms IS NULL, ping_ms DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "host_asc" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , LOWER(COALESCE(nickname, host)) ASC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "host_desc" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , LOWER(COALESCE(nickname, host)) DESC, id ASC` ;
2026-03-17 00:11:25 -05:00
case "country_asc" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , LOWER(COALESCE(country, 'zz')) ASC, id ASC` ;
2026-03-17 00:11:25 -05:00
case "country_desc" :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , LOWER(COALESCE(country, 'zz')) DESC, id ASC` ;
2026-04-07 10:36:29 -05:00
case "admin_first" :
return `ORDER BY ${ online } , ${ bookmark } , is_admin DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
default :
2026-03-22 23:21:27 -05:00
return `ORDER BY ${ online } , ${ bookmark } , last_seen DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
}
})();
const totalRow = db
. query < { c : number } > ( `SELECT COUNT(*) as c FROM clients ${ whereSql } ` )
. get (... params ) ?? { c : 0 };
const onlineRow = db
2026-03-06 20:50:02 -06:00
. query < { c : number } > (
`SELECT COUNT(*) as c FROM clients ${ whereSql ? ` ${ whereSql } AND online=1` : "WHERE online=1" } ` ,
)
. get (... params ) ?? { c : 0 };
2026-03-05 00:04:26 -06:00
const offset = ( page - 1 ) * pageSize ;
const rows = db
. query < any >(
2026-04-13 22:45:12 -05:00
`SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, build_tag as buildTag, built_by_user_id as builtByUserId, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint, cpu, gpu, ram, is_admin as isAdmin, elevation, permissions, disconnect_reason as disconnectReason, disconnect_detail as disconnectDetail
2026-03-05 00:04:26 -06:00
FROM clients
${ whereSql }
${ orderBy }
LIMIT ? OFFSET ?` ,
)
. all (... params , pageSize , offset );
const items = rows . map (( c : any ) => ({
id : c.id ,
hwid : c.hwid ,
role : ( c . role as ClientRole ) || "client" ,
lastSeen : Number ( c . lastSeen ) || 0 ,
host : c.host ,
os : c.os || "unknown" ,
arch : c.arch || "arch?" ,
version : c.version || "0" ,
user : c.user ,
2026-03-10 19:30:51 -05:00
nickname : c.nickname || null ,
2026-03-13 23:46:42 -05:00
customTag : c.customTag || null ,
customTagNote : c.customTagNote ?? null ,
2026-03-05 00:04:26 -06:00
monitors : c.monitors ,
country : c.country || "ZZ" ,
pingMs : c.pingMs ?? null ,
online : c.online === 1 ,
2026-03-17 00:11:25 -05:00
bookmarked : c.bookmarked === 1 ,
2026-04-13 22:45:12 -05:00
buildTag : c.buildTag || null ,
builtByUserId : typeof c . builtByUserId === "number" ? c.builtByUserId : null ,
2026-03-20 22:38:17 -05:00
enrollmentStatus : c.enrollmentStatus || "pending" ,
publicKey : c.publicKey || null ,
keyFingerprint : c.keyFingerprint || null ,
2026-04-01 17:27:31 -05:00
cpu : c.cpu || null ,
gpu : c.gpu || null ,
ram : c.ram || null ,
2026-04-07 10:36:29 -05:00
isAdmin : c.isAdmin === 1 ,
2026-04-08 20:59:46 -05:00
elevation : c.elevation || null ,
2026-04-13 10:49:36 -05:00
permissions : c.permissions ? (() => { try { return JSON . parse ( c . permissions ); } catch { return null ; } })() : null ,
2026-04-05 22:55:03 -05:00
disconnectReason : c.disconnectReason || null ,
disconnectDetail : c.disconnectDetail || null ,
2026-03-05 00:04:26 -06:00
thumbnail : getThumbnail ( c . id ),
}));
return { page , pageSize , total : totalRow.c , online : onlineRow.c , items };
}
2026-03-09 22:43:33 -05:00
export type ClientMetricsSummary = {
total : number ;
online : number ;
byOS : Record < string , number >;
byCountry : Record < string , number >;
byOSOnline : Record < string , number >;
byCountryOnline : Record < string , number >;
};
export function getClientMetricsSummary () : ClientMetricsSummary {
const counts = db
. query < { total : number ; online : number } > (
`SELECT COUNT(*) as total, SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online FROM clients` ,
)
. get () ?? { total : 0 , online : 0 };
const osRows = db
. query < { key : string ; total : number ; online : number } > (
`SELECT
COALESCE(NULLIF(os, ''), 'unknown') as key,
COUNT(*) as total,
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
FROM clients
GROUP BY COALESCE(NULLIF(os, ''), 'unknown')` ,
)
. all ();
const countryRows = db
. query < { key : string ; total : number ; online : number } > (
`SELECT
COALESCE(NULLIF(country, ''), 'ZZ') as key,
COUNT(*) as total,
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
FROM clients
GROUP BY COALESCE(NULLIF(country, ''), 'ZZ')` ,
)
. all ();
const byOS : Record < string , number > = {};
const byOSOnline : Record < string , number > = {};
for ( const row of osRows ) {
byOS [ row . key ] = Number ( row . total ) || 0 ;
byOSOnline [ row . key ] = Number ( row . online ) || 0 ;
}
const byCountry : Record < string , number > = {};
const byCountryOnline : Record < string , number > = {};
for ( const row of countryRows ) {
byCountry [ row . key ] = Number ( row . total ) || 0 ;
byCountryOnline [ row . key ] = Number ( row . online ) || 0 ;
}
return {
total : Number ( counts . total ) || 0 ,
online : Number ( counts . online ) || 0 ,
byOS ,
byCountry ,
byOSOnline ,
byCountryOnline ,
};
}
2026-03-05 00:04:26 -06:00
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once" ;
export type AutoScript = {
id : string ;
name : string ;
trigger : AutoScriptTrigger ;
script : string ;
scriptType : string ;
enabled : boolean ;
2026-03-17 13:27:44 -05:00
osFilter : string [];
2026-03-05 00:04:26 -06:00
createdAt : number ;
updatedAt : number ;
};
function mapAutoScriptRow ( row : any ) : AutoScript {
2026-03-17 13:27:44 -05:00
let osFilter : string [] = [];
try {
const parsed = JSON . parse ( row . os_filter || "[]" );
osFilter = Array . isArray ( parsed ) ? parsed : [];
} catch { }
2026-03-05 00:04:26 -06:00
return {
id : row.id ,
name : row.name ,
trigger : row.trigger as AutoScriptTrigger ,
script : row.script ,
scriptType : row.script_type ,
enabled : row.enabled === 1 ,
2026-03-17 13:27:44 -05:00
osFilter ,
2026-03-05 00:04:26 -06:00
createdAt : Number ( row . created_at ) || 0 ,
updatedAt : Number ( row . updated_at ) || 0 ,
};
}
export function listAutoScripts () : AutoScript [] {
const rows = db . query < any >( `SELECT * FROM auto_scripts ORDER BY created_at DESC` ). all ();
return rows . map ( mapAutoScriptRow );
}
export function getAutoScriptsByTrigger ( trigger : AutoScriptTrigger ) : AutoScript [] {
const rows = db
. query < any >(
`SELECT * FROM auto_scripts WHERE trigger=? AND enabled=1 ORDER BY created_at ASC` ,
)
. all ( trigger );
return rows . map ( mapAutoScriptRow );
}
export function getAutoScript ( id : string ) : AutoScript | null {
const row = db . query < any >( `SELECT * FROM auto_scripts WHERE id=?` ). get ( id );
return row ? mapAutoScriptRow ( row ) : null ;
}
export function createAutoScript ( input : {
id : string ;
name : string ;
trigger : AutoScriptTrigger ;
script : string ;
scriptType : string ;
enabled : boolean ;
2026-03-17 13:27:44 -05:00
osFilter : string [];
2026-03-05 00:04:26 -06:00
}) : AutoScript {
const now = Date . now ();
db . run (
2026-03-17 13:27:44 -05:00
`INSERT INTO auto_scripts (id, name, trigger, script, script_type, enabled, os_filter, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
2026-03-05 00:04:26 -06:00
,
input . id ,
input . name ,
input . trigger ,
input . script ,
input . scriptType ,
input . enabled ? 1 : 0 ,
2026-03-17 13:27:44 -05:00
JSON . stringify ( input . osFilter ?? []),
2026-03-05 00:04:26 -06:00
now ,
now ,
);
return getAutoScript ( input . id ) ! ;
}
export function updateAutoScript (
id : string ,
input : Partial < {
name : string ;
trigger : AutoScriptTrigger ;
script : string ;
scriptType : string ;
enabled : boolean ;
2026-03-17 13:27:44 -05:00
osFilter : string [];
2026-03-05 00:04:26 -06:00
} > ,
) : AutoScript | null {
const current = getAutoScript ( id );
if ( ! current ) return null ;
const next = {
name : input.name ?? current . name ,
trigger : ( input . trigger ?? current . trigger ) as AutoScriptTrigger ,
script : input.script ?? current . script ,
scriptType : input.scriptType ?? current . scriptType ,
enabled : typeof input . enabled === "boolean" ? input.enabled : current.enabled ,
2026-03-17 13:27:44 -05:00
osFilter : Array.isArray ( input . osFilter ) ? input.osFilter : current.osFilter ,
2026-03-05 00:04:26 -06:00
};
db . run (
2026-03-17 13:27:44 -05:00
`UPDATE auto_scripts SET name=?, trigger=?, script=?, script_type=?, enabled=?, os_filter=?, updated_at=? WHERE id=?`
2026-03-05 00:04:26 -06:00
,
next . name ,
next . trigger ,
next . script ,
next . scriptType ,
next . enabled ? 1 : 0 ,
2026-03-17 13:27:44 -05:00
JSON . stringify ( next . osFilter ),
2026-03-05 00:04:26 -06:00
Date . now (),
id ,
);
return getAutoScript ( id );
}
export function deleteAutoScript ( id : string ) : boolean {
const result = db . run ( `DELETE FROM auto_scripts WHERE id=?` , id );
db . run ( `DELETE FROM auto_script_runs WHERE script_id=?` , id );
return ( result as any ) ? . changes ? ( result as any ). changes > 0 : true ;
}
export function hasAutoScriptRun ( scriptId : string , clientId : string ) : boolean {
const row = db
. query < any >(
`SELECT script_id FROM auto_script_runs WHERE script_id=? AND client_id=?` ,
)
. get ( scriptId , clientId );
return !! row ? . script_id ;
}
export function recordAutoScriptRun ( scriptId : string , clientId : string ) {
db . run (
`INSERT OR REPLACE INTO auto_script_runs (script_id, client_id, ts) VALUES (?, ?, ?)`
,
scriptId ,
clientId ,
Date . now (),
);
}
export function clientExists ( id : string ) : boolean {
const row = db . query < any >( `SELECT id FROM clients WHERE id=?` ). get ( id );
return !! row ? . id ;
}
2026-03-21 00:10:47 -05:00
export function getClientPublicKeyById ( id : string ) : string | null {
const row = db . query < { public_key : string | null } > ( `SELECT public_key FROM clients WHERE id=? LIMIT 1` ). get ( id );
return row ? . public_key ?? null ;
}
2026-03-17 00:11:25 -05:00
export function listDistinctCountries () : { code : string ; count : number }[] {
const rows = db
. query < { code : string ; count : number } > (
`SELECT UPPER(COALESCE(NULLIF(country, ''), 'ZZ')) as code, COUNT(*) as count
FROM clients
GROUP BY UPPER(COALESCE(NULLIF(country, ''), 'ZZ'))
ORDER BY count DESC` ,
)
. all ();
return rows . map (( r ) => ({ code : r.code , count : Number ( r . count ) || 0 }));
}
export function setClientBookmark ( id : string , bookmarked : boolean ) : boolean {
const result = db . run (
`UPDATE clients SET bookmarked=? WHERE id=?` ,
bookmarked ? 1 : 0 ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
export function getClientBookmark ( id : string ) : boolean {
const row = db . query < { bookmarked : number } > ( `SELECT bookmarked FROM clients WHERE id=?` ). get ( id );
return row ? . bookmarked === 1 ;
}
2026-03-05 00:04:26 -06:00
export interface BuildRecord {
id : string ;
status : string ;
startTime : number ;
expiresAt : number ;
files : Array < {
name : string ;
filename : string ;
platform : string ;
2026-03-12 12:05:42 -05:00
version? : string ;
2026-03-05 00:04:26 -06:00
size : number ;
} > ;
2026-03-12 21:26:53 -05:00
buildTag? : string ;
builtByUserId? : number ;
2026-03-05 00:04:26 -06:00
}
export function saveBuild ( build : BuildRecord ) {
db . run (
2026-03-12 21:26:53 -05:00
`INSERT OR REPLACE INTO builds (id, status, start_time, expires_at, files, build_tag, built_by_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)` ,
2026-03-05 00:04:26 -06:00
build . id ,
build . status ,
build . startTime ,
build . expiresAt ,
JSON . stringify ( build . files ),
2026-03-12 21:26:53 -05:00
build . buildTag || null ,
build . builtByUserId || null ,
2026-03-05 00:04:26 -06:00
);
}
export function getBuild ( id : string ) : BuildRecord | null {
const row = db . query < any >( `SELECT * FROM builds WHERE id = ?` ). get ( id );
if ( ! row ) return null ;
return {
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
2026-03-12 21:26:53 -05:00
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
};
}
export function getBuildByTag ( buildTag : string ) : BuildRecord | null {
const row = db . query < any >( `SELECT * FROM builds WHERE build_tag = ?` ). get ( buildTag );
if ( ! row ) return null ;
return {
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
2026-03-05 00:04:26 -06:00
};
}
2026-03-16 13:42:13 +01:00
export function getAllBuilds ( userId? : number , role? : string ) : BuildRecord [] {
2026-03-16 18:48:41 -05:00
if ( role !== undefined && role !== "admin" ) {
2026-03-16 18:42:56 -05:00
if ( userId == null ) {
return [];
}
2026-03-16 13:42:13 +01:00
const rows = db
. query < any >( `SELECT * FROM builds WHERE built_by_user_id = ? ORDER BY start_time DESC` )
. all ( userId );
return rows . map (( row ) => ({
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
}));
}
2026-03-05 00:04:26 -06:00
const rows = db
2026-03-16 18:42:56 -05:00
. query < any >( "SELECT * FROM builds ORDER BY start_time DESC" )
2026-03-05 00:04:26 -06:00
. all ();
return rows . map (( row ) => ({
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
2026-03-12 21:26:53 -05:00
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
2026-03-05 00:04:26 -06:00
}));
}
export function deleteExpiredBuilds() {
const now = Date . now ();
db . run ( `DELETE FROM builds WHERE expires_at <= ?` , now );
}
export function deleteBuild ( id : string ) {
db . run ( `DELETE FROM builds WHERE id = ?` , id );
}
export interface NotificationScreenshotRecord {
id : string ;
notificationId : string ;
clientId : string ;
ts : number ;
format : string ;
width? : number ;
height? : number ;
bytes : Uint8Array ;
}
export function saveNotificationScreenshot ( record : NotificationScreenshotRecord ) {
db . run (
`INSERT OR REPLACE INTO notification_screenshots
(id, notification_id, client_id, ts, format, width, height, bytes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
,
record . id ,
record . notificationId ,
record . clientId ,
record . ts ,
record . format ,
record . width ?? null ,
record . height ?? null ,
record . bytes ,
);
}
export function getNotificationScreenshot ( notificationId : string ) : NotificationScreenshotRecord | null {
const row = db
. query < any >(
`SELECT * FROM notification_screenshots WHERE notification_id = ? ORDER BY ts DESC LIMIT 1` ,
)
. get ( notificationId );
if ( ! row ) return null ;
return {
id : row.id ,
notificationId : row.notification_id ,
clientId : row.client_id ,
ts : row.ts ,
format : row.format ,
width : row.width ?? undefined ,
height : row.height ?? undefined ,
bytes : row.bytes ,
};
}
export function clearNotificationScreenshots() {
db . run ( `DELETE FROM notification_screenshots` );
console . log ( "[db] cleared notification screenshots" );
}
2026-03-20 22:38:17 -05:00
export function getClientEnrollmentStatus ( id : string ) : string | null {
const row = db
. query < { enrollment_status : string } > (
`SELECT enrollment_status FROM clients WHERE id=?` ,
)
. get ( id );
return row ? . enrollment_status ?? null ;
}
export function setClientEnrollmentStatus (
id : string ,
status : "approved" | "denied" | "pending" ,
approvedBy? : string ,
) : boolean {
const result = db . run (
`UPDATE clients SET enrollment_status=?, enrolled_at=?, enrolled_by=? WHERE id=?` ,
status ,
status === "approved" ? Date . now () : null ,
status === "approved" ? ( approvedBy ?? null ) : null ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
export function lookupClientByPublicKey (
publicKey : string ,
) : { id : string ; enrollmentStatus : string } | null {
const row = db
. query < { id : string ; enrollment_status : string } > (
`SELECT id, enrollment_status FROM clients WHERE public_key=? LIMIT 1` ,
)
. get ( publicKey );
if ( ! row ) return null ;
return { id : row.id , enrollmentStatus : row.enrollment_status };
}
2026-04-13 14:11:49 -05:00
export function getEnrollmentStats ( opts ?: {
allowedClientIds? : string [];
deniedClientIds? : string [];
2026-04-13 22:45:12 -05:00
builtByUserId? : number ;
requireBuildOwner? : boolean ;
2026-04-13 14:11:49 -05:00
}) : {
2026-03-20 22:38:17 -05:00
pending : number ;
approved : number ;
denied : number ;
} {
2026-04-13 14:11:49 -05:00
let sql = `SELECT COALESCE(enrollment_status,'pending') as status, COUNT(*) as c FROM clients` ;
const params : any [] = [];
2026-04-13 22:45:12 -05:00
const where : string [] = [];
2026-04-13 14:11:49 -05:00
if ( opts ? . allowedClientIds ) {
if ( opts . allowedClientIds . length === 0 ) return { pending : 0 , approved : 0 , denied : 0 };
const placeholders = opts . allowedClientIds . map (() => "?" ). join ( "," );
2026-04-13 22:45:12 -05:00
where . push ( `id IN ( ${ placeholders } )` );
2026-04-13 14:11:49 -05:00
params . push (... opts . allowedClientIds );
2026-04-13 22:45:12 -05:00
}
if ( opts ? . deniedClientIds && opts . deniedClientIds . length > 0 ) {
2026-04-13 14:11:49 -05:00
const placeholders = opts . deniedClientIds . map (() => "?" ). join ( "," );
2026-04-13 22:45:12 -05:00
where . push ( `id NOT IN ( ${ placeholders } )` );
2026-04-13 14:11:49 -05:00
params . push (... opts . deniedClientIds );
}
2026-04-13 22:45:12 -05:00
if ( typeof opts ? . builtByUserId === "number" ) {
where . push ( "built_by_user_id = ?" );
params . push ( opts . builtByUserId );
}
if ( opts ? . requireBuildOwner ) {
where . push ( "built_by_user_id IS NOT NULL" );
}
if ( where . length > 0 ) {
sql += ` WHERE ${ where . join ( " AND " ) } ` ;
}
2026-04-13 14:11:49 -05:00
sql += ` GROUP BY enrollment_status` ;
const rows = db . query < { status : string ; c : number } > ( sql ). all (... params );
2026-03-20 22:38:17 -05:00
const stats = { pending : 0 , approved : 0 , denied : 0 };
for ( const r of rows ) {
if ( r . status === "approved" ) stats . approved = Number ( r . c );
else if ( r . status === "denied" ) stats . denied = Number ( r . c );
else stats . pending = Number ( r . c );
}
return stats ;
}
2026-03-23 20:24:35 +01:00
export interface PushSubscriptionRecord {
id : number ;
userId : number ;
endpoint : string ;
p256dh : string ;
auth : string ;
createdAt : number ;
}
export function savePushSubscription ( userId : number , endpoint : string , p256dh : string , auth : string ) : void {
db . run (
`INSERT OR REPLACE INTO push_subscriptions (user_id, endpoint, p256dh, auth, created_at)
VALUES (?, ?, ?, ?, ?)` ,
userId , endpoint , p256dh , auth , Date . now (),
);
}
export function deletePushSubscription ( endpoint : string ) : void {
db . run ( `DELETE FROM push_subscriptions WHERE endpoint=?` , endpoint );
}
export function deletePushSubscriptionsByUser ( userId : number ) : void {
db . run ( `DELETE FROM push_subscriptions WHERE user_id=?` , userId );
}
export function getPushSubscriptionsByUser ( userId : number ) : PushSubscriptionRecord [] {
return db
. query < any >( `SELECT * FROM push_subscriptions WHERE user_id=?` )
. all ( userId )
. map (( r : any ) => ({
id : r.id ,
userId : r.user_id ,
endpoint : r.endpoint ,
p256dh : r.p256dh ,
auth : r.auth ,
createdAt : Number ( r . created_at ) || 0 ,
}));
}
export function getAllPushSubscriptions () : PushSubscriptionRecord [] {
return db
. query < any >( `SELECT * FROM push_subscriptions` )
. all ()
. map (( r : any ) => ({
id : r.id ,
userId : r.user_id ,
endpoint : r.endpoint ,
p256dh : r.p256dh ,
auth : r.auth ,
createdAt : Number ( r . created_at ) || 0 ,
}));
}
2026-04-13 14:11:49 -05:00
export function getPendingClients ( opts ?: {
allowedClientIds? : string [];
deniedClientIds? : string [];
2026-04-13 22:45:12 -05:00
builtByUserId? : number ;
requireBuildOwner? : boolean ;
2026-04-13 14:11:49 -05:00
}) : {
2026-03-20 22:38:17 -05:00
id : string ;
host : string | null ;
os : string | null ;
user : string | null ;
ip : string | null ;
country : string | null ;
publicKey : string | null ;
keyFingerprint : string | null ;
lastSeen : number ;
}[] {
2026-04-13 14:11:49 -05:00
let sql = `SELECT id, host, os, user, ip, country, public_key as publicKey, key_fingerprint as keyFingerprint, last_seen as lastSeen
FROM clients WHERE (enrollment_status='pending' OR enrollment_status IS NULL)` ;
const params : any [] = [];
if ( opts ? . allowedClientIds ) {
if ( opts . allowedClientIds . length === 0 ) return [];
const placeholders = opts . allowedClientIds . map (() => "?" ). join ( "," );
sql += ` AND id IN ( ${ placeholders } )` ;
params . push (... opts . allowedClientIds );
2026-04-13 22:45:12 -05:00
}
if ( opts ? . deniedClientIds && opts . deniedClientIds . length > 0 ) {
2026-04-13 14:11:49 -05:00
const placeholders = opts . deniedClientIds . map (() => "?" ). join ( "," );
sql += ` AND id NOT IN ( ${ placeholders } )` ;
params . push (... opts . deniedClientIds );
}
2026-04-13 22:45:12 -05:00
if ( typeof opts ? . builtByUserId === "number" ) {
sql += ` AND built_by_user_id = ?` ;
params . push ( opts . builtByUserId );
}
if ( opts ? . requireBuildOwner ) {
sql += ` AND built_by_user_id IS NOT NULL` ;
}
2026-04-13 14:11:49 -05:00
sql += ` ORDER BY last_seen DESC` ;
2026-03-20 22:38:17 -05:00
return db
2026-04-13 14:11:49 -05:00
. query < any >( sql )
. all (... params )
2026-03-20 22:38:17 -05:00
. map (( r : any ) => ({
id : r.id ,
host : r.host ,
os : r.os ,
user : r.user ,
ip : r.ip ,
country : r.country ,
publicKey : r.publicKey ,
keyFingerprint : r.keyFingerprint ,
lastSeen : Number ( r . lastSeen ) || 0 ,
}));
}
2026-03-29 17:53:25 -05:00
2026-04-13 22:45:12 -05:00
export function getClientBuildOwnership (
id : string ,
) : { buildTag : string | null ; builtByUserId : number | null } | null {
const row = db
. query < { build_tag : string | null ; built_by_user_id : number | null } > (
`SELECT build_tag, built_by_user_id FROM clients WHERE id=?` ,
)
. get ( id );
if ( ! row ) return null ;
return {
buildTag : row.build_tag ?? null ,
builtByUserId :
typeof row . built_by_user_id === "number" ? row.built_by_user_id : null ,
};
}
2026-03-29 17:53:25 -05:00
export type AutoDeployTrigger = "on_connect" | "on_first_connect" | "on_connect_once" ;
export type AutoDeploy = {
id : string ;
name : string ;
trigger : AutoDeployTrigger ;
filePath : string ;
fileName : string ;
fileSize : number ;
fileOs : string ;
args : string ;
hideWindow : boolean ;
enabled : boolean ;
osFilter : string [];
createdAt : number ;
updatedAt : number ;
};
function mapAutoDeployRow ( row : any ) : AutoDeploy {
let osFilter : string [] = [];
try {
const parsed = JSON . parse ( row . os_filter || "[]" );
osFilter = Array . isArray ( parsed ) ? parsed : [];
} catch { }
return {
id : row.id ,
name : row.name ,
trigger : row.trigger as AutoDeployTrigger ,
filePath : row.file_path ,
fileName : row.file_name ,
fileSize : Number ( row . file_size ) || 0 ,
fileOs : row.file_os || "unknown" ,
args : row.args || "" ,
hideWindow : row.hide_window === 1 ,
enabled : row.enabled === 1 ,
osFilter ,
createdAt : Number ( row . created_at ) || 0 ,
updatedAt : Number ( row . updated_at ) || 0 ,
};
}
export function listAutoDeploys () : AutoDeploy [] {
const rows = db . query < any >( `SELECT * FROM auto_deploys ORDER BY created_at DESC` ). all ();
return rows . map ( mapAutoDeployRow );
}
export function getAutoDeploysByTrigger ( trigger : AutoDeployTrigger ) : AutoDeploy [] {
const rows = db
. query < any >(
`SELECT * FROM auto_deploys WHERE trigger=? AND enabled=1 ORDER BY created_at ASC` ,
)
. all ( trigger );
return rows . map ( mapAutoDeployRow );
}
export function getAutoDeploy ( id : string ) : AutoDeploy | null {
const row = db . query < any >( `SELECT * FROM auto_deploys WHERE id=?` ). get ( id );
return row ? mapAutoDeployRow ( row ) : null ;
}
export function createAutoDeploy ( input : {
id : string ;
name : string ;
trigger : AutoDeployTrigger ;
filePath : string ;
fileName : string ;
fileSize : number ;
fileOs : string ;
args : string ;
hideWindow : boolean ;
enabled : boolean ;
osFilter : string [];
}) : AutoDeploy {
const now = Date . now ();
db . run (
`INSERT INTO auto_deploys (id, name, trigger, file_path, file_name, file_size, file_os, args, hide_window, enabled, os_filter, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
input . id ,
input . name ,
input . trigger ,
input . filePath ,
input . fileName ,
input . fileSize ,
input . fileOs ,
input . args ,
input . hideWindow ? 1 : 0 ,
input . enabled ? 1 : 0 ,
JSON . stringify ( input . osFilter ?? []),
now ,
now ,
);
return getAutoDeploy ( input . id ) ! ;
}
export function updateAutoDeploy (
id : string ,
input : Partial < {
name : string ;
trigger : AutoDeployTrigger ;
args : string ;
hideWindow : boolean ;
enabled : boolean ;
osFilter : string [];
} > ,
) : AutoDeploy | null {
const current = getAutoDeploy ( id );
if ( ! current ) return null ;
const next = {
name : input.name ?? current . name ,
trigger : ( input . trigger ?? current . trigger ) as AutoDeployTrigger ,
args : input.args ?? current . args ,
hideWindow : typeof input . hideWindow === "boolean" ? input.hideWindow : current.hideWindow ,
enabled : typeof input . enabled === "boolean" ? input.enabled : current.enabled ,
osFilter : Array.isArray ( input . osFilter ) ? input.osFilter : current.osFilter ,
};
db . run (
`UPDATE auto_deploys SET name=?, trigger=?, args=?, hide_window=?, enabled=?, os_filter=?, updated_at=? WHERE id=?` ,
next . name ,
next . trigger ,
next . args ,
next . hideWindow ? 1 : 0 ,
next . enabled ? 1 : 0 ,
JSON . stringify ( next . osFilter ),
Date . now (),
id ,
);
return getAutoDeploy ( id );
}
export function deleteAutoDeploy ( id : string ) : boolean {
const result = db . run ( `DELETE FROM auto_deploys WHERE id=?` , id );
db . run ( `DELETE FROM auto_deploy_runs WHERE deploy_id=?` , id );
return ( result as any ) ? . changes ? ( result as any ). changes > 0 : true ;
}
export function hasAutoDeployRun ( deployId : string , clientId : string ) : boolean {
const row = db
. query < any >(
`SELECT deploy_id FROM auto_deploy_runs WHERE deploy_id=? AND client_id=?` ,
)
. get ( deployId , clientId );
return !! row ? . deploy_id ;
}
export function recordAutoDeployRun ( deployId : string , clientId : string ) {
db . run (
`INSERT OR REPLACE INTO auto_deploy_runs (deploy_id, client_id, ts) VALUES (?, ?, ?)` ,
deployId ,
clientId ,
Date . now (),
);
}