feat: cookie Secure conditionnel + sous-commande d'installation de service

Cookie: routes/auth.ts pose `secure` sur le cookie de session quand la requête
arrive en HTTPS (x-forwarded-proto), sans trustProxy — durcit le cookie derrière
Tailscale Serve sans casser le localhost http.

Install: nouveau cli/install.ts + routeur de sous-commandes dans index.ts
(install/uninstall/status/serve). Service utilisateur systemd (Linux) ou launchd
(macOS), bootstrap du token, --dry-run/--no-enable. Rétrocompat stricte du daemon
par défaut (runDaemon extrait).

Tests: app.e2e (cookie Secure local vs HTTPS) + cli-install (fonctions pures).
203/203 verts, acceptation P1/P4 vertes.

Docs: README.md + README.fr.md (installeur multi-OS, distinction utiliser/cloner,
modèle de sécurité durci).
This commit is contained in:
2026-06-17 16:17:08 +02:00
parent 11c45ff060
commit f446a3dda1
7 changed files with 694 additions and 21 deletions

View File

@@ -2,16 +2,17 @@
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadConfig } from './config.js';
import { loadConfig, type Config } from './config.js';
import { openDb } from './db/index.js';
import { buildApp } from './app.js';
import { runInstall, runUninstall, runStatus, printUsage, printTokenBanner } from './cli/install.js';
const pkg = JSON.parse(
readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
) as { version: string };
async function main(): Promise<void> {
const config = loadConfig();
/** Démarre le daemon : écoute HTTP, scan des sessions, drain propre au SIGTERM/SIGINT. */
export async function runDaemon(config: Config): Promise<void> {
const db = openDb(config.dbPath);
const { app, auth, manager, discovery } = buildApp(config, db, pkg.version);
@@ -23,11 +24,7 @@ async function main(): Promise<void> {
app.log.info(`Arboretum v${pkg.version}${url}`);
if (bootstrapToken) {
// Affiché une seule fois : le hash seul est stocké.
console.log('\n┌──────────────────────────────────────────────────────────────────┐');
console.log('│ First start — your access token (shown once, store it safely): │');
console.log('└──────────────────────────────────────────────────────────────────┘');
console.log(`\n ${bootstrapToken}\n`);
console.log(` Login at: ${url}/\n`);
printTokenBanner(bootstrapToken, url);
} else if (config.printToken) {
console.log('Tokens are stored hashed and cannot be re-printed. Create a new one from Settings (or reset the db).');
}
@@ -47,6 +44,36 @@ async function main(): Promise<void> {
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
// Routeur de sous-commandes. On inspecte argv[0] AVANT loadConfig (parseArgs strict throw sur
// un positionnel inconnu). Aucune sous-commande (ou un flag en tête) → daemon : rétrocompat stricte
// de `arboretum`, `arboretum --port 8080`, `npx @johanleroy/git-arboretum --allow-origin …`.
async function main(): Promise<void> {
const argv = process.argv.slice(2);
const cmd = argv[0];
switch (cmd) {
case 'install':
return runInstall(argv.slice(1));
case 'uninstall':
return runUninstall(argv.slice(1));
case 'status':
return runStatus(argv.slice(1));
case 'serve':
return runDaemon(loadConfig(argv.slice(1)));
case 'help':
case '--help':
case '-h':
printUsage(pkg.version);
return;
default:
if (cmd && !cmd.startsWith('-')) {
console.error(`Unknown command: ${cmd}\n`);
printUsage(pkg.version);
process.exit(1);
}
return runDaemon(loadConfig(argv));
}
}
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);