File size: 2,715 Bytes
077865a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Bundled by scripts/bundle-server.mjs into build/server.mjs (ESM, only
// better-sqlite3 external). This is the ONLY module allowed to touch server
// internals: the db singleton lives inside this bundle, so anything stateful
// (auth bootstrap, getDb) must be exported from here rather than imported
// from server/src by the main bundle (which would get a second, empty copy).
//
// The server sources live in this same repo (../../server). Keep these
// relative imports in sync with the repo-root default in main.ts.
import '../../server/src/env.js';
import crypto from 'node:crypto';
import type { Server } from 'node:http';
import { createApp } from '../../server/src/app.js';
import { initDb, getDb, getUnifiedApiKey } from '../../server/src/db/index.js';
import { startHealthChecker } from '../../server/src/services/health.js';
import { userCount, createUser, createSession } from '../../server/src/services/auth.js';

export { getDb, getUnifiedApiKey };

export interface StartOptions {
  dbPath: string;
  clientDist: string;
  host: string;
  preferredPort: number;
}

export interface ServerHandle {
  server: Server;
  port: number;
}

export async function startServer(opts: StartOptions): Promise<ServerHandle> {
  process.env.CLIENT_DIST = opts.clientDist;
  initDb(opts.dbPath);
  const app = createApp();
  const { server, port } = await listenWithScan(app, opts.host, opts.preferredPort);
  startHealthChecker();
  return { server, port };
}

// The dashboard window authenticates as a hidden machine user. The password
// is random and never shown — sessions are minted directly against the DB.
export function ensureSessionToken(): string {
  if (userCount() === 0) {
    createUser('desktop@localhost', crypto.randomBytes(24).toString('hex'));
  }
  const first = getDb().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get() as { id: number };
  return createSession(first.id);
}

async function listenWithScan(
  app: ReturnType<typeof createApp>,
  host: string,
  start: number,
  attempts = 50,
): Promise<{ server: Server; port: number }> {
  for (let port = start; port < start + attempts; port++) {
    const server = await tryListen(app, host, port);
    if (server) return { server, port };
  }
  throw new Error(`No free port found in ${start}${start + attempts - 1}`);
}

function tryListen(app: ReturnType<typeof createApp>, host: string, port: number): Promise<Server | null> {
  return new Promise((resolve) => {
    const server = app.listen(port, host);
    server.once('listening', () => resolve(server));
    server.once('error', (err: NodeJS.ErrnoException) => {
      if (err.code === 'EADDRINUSE') resolve(null);
      else resolve(null);
    });
  });
}