P1 spine: monorepo, shared WS protocol, server daemon

- npm workspaces (shared / server / web), TS strict, project refs
- @arboretum/shared: multiplexed WS protocol (JSON control + binary
  output frames: 1B type + u32le channel), flow-control constants
  (ACK 256K, HIGH 384K, LOW 128K, lagging 2M), REST types
- git-arboretum server: Fastify 5 + node:sqlite (single native dep:
  node-pty prebuilt), token auth (sha256 at rest, HMAC cookie, global
  login rate limit + backoff), strict Origin check on /api and /ws,
  PtyManager (2MiB ring with monotonic offset, resync replay = reset +
  256KiB tail, pause/resume only when ALL interactive clients exceed
  HIGH, observers never throttle, lagging clients resync), WS gateway
  (attach/stdin/resize/ack, heartbeat 30s), SIGTERM→SIGKILL 5s grace
- CLI: arboretum [--port 7317] [--bind 127.0.0.1] — non-loopback bind
  requires an explicit safety flag
- Smoke-tested: login/401/403-origin/spawn bash/kill/grace-SIGKILL all
  green
This commit is contained in:
2026-06-11 22:04:09 +02:00
parent 903d5deeb5
commit 4768b606e4
25 changed files with 4467 additions and 474 deletions

View File

@@ -0,0 +1,42 @@
import type { FastifyInstance } from 'fastify';
import type { LoginRequest, LoginResponse, MeResponse } from '@arboretum/shared';
import type { AuthService, LoginRateLimiter } from '../auth/service.js';
export function registerAuthRoutes(
app: FastifyInstance,
auth: AuthService,
limiter: LoginRateLimiter,
serverVersion: string,
): void {
app.post('/api/v1/auth/login', { config: { public: true } }, async (req, reply) => {
const wait = limiter.check();
if (wait !== null) {
return reply.status(429).send({ error: { code: 'RATE_LIMITED', message: `Retry in ${Math.ceil(wait / 1000)}s` } });
}
const body = req.body as Partial<LoginRequest> | null;
const ctx = typeof body?.token === 'string' ? auth.verifyRawToken(body.token) : null;
if (!ctx) {
limiter.recordFailure();
return reply.status(401).send({ error: { code: 'BAD_TOKEN', message: 'Invalid token' } });
}
limiter.recordSuccess();
void reply.setCookie(auth.cookieName, auth.issueCookie(ctx), {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: 30 * 24 * 3600,
});
const res: LoginResponse = { ok: true, label: ctx.label };
return reply.send(res);
});
app.get('/api/v1/auth/me', async (req, reply) => {
const res: MeResponse = { ok: true, tokenLabel: req.authContext?.label ?? 'unknown', serverVersion };
return reply.send(res);
});
app.post('/api/v1/auth/logout', async (_req, reply) => {
void reply.clearCookie(auth.cookieName, { path: '/' });
return reply.send({ ok: true });
});
}

View File

@@ -0,0 +1,35 @@
import type { FastifyInstance } from 'fastify';
import type { CreateSessionRequest, SessionResponse, SessionsListResponse } from '@arboretum/shared';
import type { PtyManager } from '../core/pty-manager.js';
export function registerSessionRoutes(app: FastifyInstance, manager: PtyManager): void {
app.get('/api/v1/sessions', async (): Promise<SessionsListResponse> => {
return { sessions: manager.list() };
});
app.post('/api/v1/sessions', async (req, reply) => {
const body = req.body as Partial<CreateSessionRequest> | null;
if (!body || typeof body.cwd !== 'string' || !body.cwd.startsWith('/')) {
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'cwd (absolute path) is required' } });
}
if (body.command !== undefined && body.command !== 'claude' && body.command !== 'bash') {
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'command must be claude or bash' } });
}
try {
const session = manager.spawn({ cwd: body.cwd, ...(body.command ? { command: body.command } : {}) });
const res: SessionResponse = { session };
return reply.status(201).send(res);
} catch (err) {
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
return reply.status(statusCode).send({ error: { code: 'SPAWN_FAILED', message: (err as Error).message } });
}
});
app.delete('/api/v1/sessions/:id', async (req, reply) => {
const { id } = req.params as { id: string };
if (!manager.kill(id)) {
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No live session with this id' } });
}
return reply.send({ ok: true });
});
}