feat: plan-approval overlay, auth-expiry handling, cert download, login reset

- Add a dedicated plan-approval bottom sheet (interactive-prompt) driven by
  ExitPlanMode's now-4-option layout (Claude Code v2.1.177), with
  approve/YOLO/reject/feedback wired through respondPlan(requestId).
- PermissionManager: stop auto-expiring AskUserQuestion/plan prompts and stop
  releasing pending prompts on client disconnect — they reflect real CLI
  state and must survive reconnects.
- Detect server-rejected tokens (401/403 or failed WS handshake) and bounce
  back to the login screen via a new AUTH_EXPIRED_EVENT.
- Add `/cert` endpoint + Settings entry so phones can download/trust the
  self-signed HTTPS cert directly; fix cert IP detection on non-macOS hosts.
- Add `clawtap reset-login` command and `/api/auth/reset-attempts` endpoint
  to clear login rate-limit lockouts.
- Codex adapter: only forward recognized keystrokes/option indices to tmux,
  never the synthetic "deny" dismissal signal.
- PWA: actively poll for service-worker updates (iOS standalone apps don't
  reliably check on navigation).
- update-service.sh: install tmux if missing, prompt to create the env file
  instead of failing, add --skip-firewall, and improve logging.
This commit is contained in:
2026-06-15 16:30:24 -04:00
parent 4e6dfb4726
commit 2ded310472
23 changed files with 656 additions and 97 deletions
+22 -1
View File
@@ -10,6 +10,7 @@ import {
initAuth,
login,
authMiddleware,
resetLoginAttempts,
} from './auth.js';
import './adapters/init.js';
import { initAll, installAllHooks, listAvailable, get as getAdapter, getAll as getAllAdapters, cleanupAll, DEFAULT_ADAPTER } from './adapters/registry.js';
@@ -31,7 +32,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
import multer from 'multer';
import { randomUUID } from 'crypto';
import { mkdirSync, writeFileSync, unlinkSync } from 'fs';
import { mkdirSync, writeFileSync, unlinkSync, existsSync } from 'fs';
import type { Server as HttpServer } from 'http';
import type { Server as HttpsServer } from 'https';
@@ -73,6 +74,18 @@ async function start(): Promise<void> {
res.json({ status: 'ok', branch: config.gitBranch, version: pkg.version });
});
// Serve the self-signed cert so phones can download + trust it directly
// (no auth — the device needs this before it can trust the HTTPS connection).
app.get('/cert', (_req: Request, res: Response) => {
const certPath = join(config.clawtapDir, 'cert.pem');
if (!config.https || !existsSync(certPath)) {
return res.status(404).json({ error: 'No certificate configured. Run `clawtap cert` to generate one.' });
}
res.setHeader('Content-Type', 'application/x-x509-ca-cert');
res.setHeader('Content-Disposition', 'attachment; filename="clawtap-ca.pem"');
res.sendFile(certPath, { dotfiles: 'allow' });
});
app.post('/api/auth/login', async (req: Request, res: Response) => {
const { password } = req.body as { password?: string };
if (!password) {
@@ -86,6 +99,14 @@ async function start(): Promise<void> {
res.json({ token: (result as { token: string }).token });
});
// Manual reset of login rate limiting. Restricted to localhost (via authMiddleware's
// bypass) since a locked-out user has no token but can run `clawtap reset-login` locally.
app.post('/api/auth/reset-attempts', authMiddleware, (req: Request, res: Response) => {
const { ip } = req.body as { ip?: string };
resetLoginAttempts(ip);
res.json({ ok: true });
});
app.get('/api/sessions', authMiddleware, async (req: Request, res: Response) => {
try {
const { dir, limit } = req.query as { dir?: string; limit?: string };