#!/usr/bin/env node /** * Standalone orchestrator entry (`dshs` bin). * * Subcommands: * dshs bootstrap-admin --username --password

* dshs [server flags] * @module dshs/cli */ import { randomUUID } from 'node:crypto' import { parseArgs } from 'node:util' import { resolveConfig, type ConfigOverrides } from './config.js' import { createDbAdapter } from './db/index.js' import { createUserFs } from './fs/provider.js' import { homeRoot, userRoot } from './fs/workspace.js' import { hashPassword } from './web/auth.js' import { hashUid } from './isolation.js' import { buildServer } from './web/server.js' const HELP = `dshs — DSH server login orchestrator Usage: dshs [options] start the server dshs bootstrap-admin [options] create the first admin Server options: --port Bind port (0 = ephemeral). Default 3080. --host Bind host. Default 127.0.0.1. --db SQLite database path. --data-root

Root for per-user homes + workspaces. --dsh-bin Command used to launch a child DSH. Default "dsh". --log-level Pino log level. Default "info". --secure-cookies Set the Secure flag on session cookies (behind HTTPS). --session-ttl Session lifetime in seconds. Default 604800 (7 days). --isolation-mode Isolation tier: "soft" or "account" (Linux, needs root). Default "soft". -h, --help Show this help. bootstrap-admin options: --username Admin username (required). --password

Admin password (or DSHS_ADMIN_PASSWORD env). --db Database path. --data-root

Root for per-user homes. ` interface ParsedValues { [key: string]: string | boolean | undefined } function toOverrides(values: ParsedValues): ConfigOverrides { const str = (value: string | boolean | undefined): string | undefined => typeof value === 'string' ? value : undefined const dshBin = str(values['dsh-bin']) return { port: str(values.port), host: str(values.host), dbPath: str(values.db), dataRoot: str(values['data-root']), dshCommand: dshBin ? [dshBin] : undefined, logLevel: str(values['log-level']), secureCookies: values['secure-cookies'] === true ? true : undefined, sessionTtlSeconds: str(values['session-ttl']), maxUploadBytes: str(values['max-upload']), isolationMode: str(values['isolation-mode']), } } async function bootstrapAdmin(args: string[]): Promise { const { values } = parseArgs({ args, options: { username: { type: 'string' }, password: { type: 'string' }, db: { type: 'string' }, 'data-root': { type: 'string' }, }, }) const username = values.username const password = values.password ?? process.env.DSHS_ADMIN_PASSWORD if (username === undefined || username === '' || password === undefined || password === '') { console.error('usage: dshs bootstrap-admin --username --password

') process.exit(2) } const config = resolveConfig({ dbPath: values.db, dataRoot: values['data-root'] }) const db = await createDbAdapter(config) if ((await db.countAdmins()) > 0) { console.error('an admin already exists; refusing to create a second one') await db.close() process.exit(1) } const id = randomUUID() const homeDir = homeRoot(userRoot(config.dataRoot, id)) const passHash = await hashPassword(password) // Create the user before initUserRoot so the per-user uid (baseUid + row_id) // is already assigned — same reason as the register route. const user = await db.createUser({ id, username, passHash, role: 'admin', homeDir }) await createUserFs(config).initUserRoot(id, user.uid ?? undefined) await db.close() console.log(`admin "${username}" created (id: ${id})`) } async function runServer(args: string[]): Promise { const { values } = parseArgs({ args, options: { port: { type: 'string' }, host: { type: 'string' }, db: { type: 'string' }, 'data-root': { type: 'string' }, 'dsh-bin': { type: 'string' }, 'log-level': { type: 'string' }, 'secure-cookies': { type: 'boolean' }, 'session-ttl': { type: 'string' }, 'max-upload': { type: 'string' }, 'isolation-mode': { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, }) if (values.help) { process.stdout.write(HELP) return } const config = resolveConfig(toOverrides(values as ParsedValues)) const app = await buildServer(config) await app.listen({ host: config.host, port: config.port }) const address = app.server.address() const actualPort = typeof address === 'object' && address !== null ? address.port : config.port app.log.info(`dshs listening on http://${config.host}:${actualPort}`) app.log.info(`data root: ${config.dataRoot}; db: ${config.dbPath}`) const shutdown = async (signal: string): Promise => { app.log.info(`received ${signal}, shutting down`) await app.close() process.exit(0) } process.on('SIGINT', () => void shutdown('SIGINT')) process.on('SIGTERM', () => void shutdown('SIGTERM')) } async function uidForUserCmd(args: string[]): Promise { const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { 'base-uid': { type: 'string' }, db: { type: 'string' } }, }) const userId = positionals[0] if (userId === undefined) { console.error('usage: dshs uid-for-user [--db ] [--base-uid N]') process.exit(2) } const dbPath = typeof values.db === 'string' ? values.db : undefined const baseUid = typeof values['base-uid'] === 'string' ? values['base-uid'] : undefined const config = resolveConfig({ dbPath, baseUid }) const db = await createDbAdapter(config) const user = await db.findUserById(userId) await db.close() console.log(user?.uid ?? hashUid(userId, config.baseUid)) } async function main(): Promise { const [first, ...rest] = process.argv.slice(2) if (first === 'bootstrap-admin') { await bootstrapAdmin(rest) return } if (first === 'uid-for-user') { await uidForUserCmd(rest) return } await runServer(process.argv.slice(2)) } main().catch((err: unknown) => { console.error(err) process.exit(1) })