CRM API — Referencia de endpoints
Arc OS — The Orchestration System for AI Teams
Información general
| Parámetro | Valor |
|---|---|
| Base URL | https://arc-os.co/api/crm |
| Autorización | Authorization: Bearer <JWT> o ?token=<JWT> (para SSE/WebSocket) |
| Content-Type | application/json |
| Algoritmo JWT | HMAC-SHA256 |
| TTL JWT | 24 horas |
Autenticación
Todos los endpoints (excepto /docs/*) requieren un token JWT en el header Authorization: Bearer <token>.
Para conexiones SSE y WebSocket el token se pasa mediante el query parameter ?token=<JWT>.
Errores de autorización
| Código | Descripción |
|---|---|
| 401 | Token ausente o inválido |
| 403 | Sin acceso al proyecto (multi-tenancy) |
Endpoints por categoría
Cuenta y ajustes
| Método | Ruta | Descripción |
|---|---|---|
| GET | /account/settings |
Obtener ajustes de cuenta |
| PUT | /account/settings |
Actualizar ajustes de cuenta |
| GET | /account/usage |
Historial de uso de tokens para el usuario autorizado (Phase 63, #148). Response: { rows: [ { project_name, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at } × hasta 200 ], totals: { total, input, output } }. Lee token_usage_log por owner_id. Mostrado en UserDropdown (UsageCard) y BillingPage (sección Token Usage). |
Onboarding + Trial Credits (Phase 50.1)
| Método | Ruta | Descripción |
|---|---|---|
| POST | /onboarding/setup |
Crear el primer proyecto. Body multipart: config (JSON) + files. El campo anthropicKey ahora es opcional — si está vacío + el usuario tiene email verificado + no ha recibido prueba gratuita antes, el proyecto se crea en trial_mode=1 con 100K free tokens. Response: { ok, project, trial_activated }. Phase 51: devuelve 402 con {error:"plan_limit_reached", reason, current, limit, plan} cuando el usuario supera el límite de proyectos del plan. |
| GET | /account/trial-status |
Estado de prueba gratuita para el banner de UI. Response: { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] } |
Onboarding Checklist (Phase 54.1, issue #56)
Checklist de engagement post-wizard con 5 pasos. Cada paso (workers, cli, skill, bot, issue) acepta el estado completed o skipped. Las mutaciones son idempotentes: un POST idéntico repetido devuelve el mismo estado sin escribir un duplicado en activity_log. El replay no reinicia el estado, solo elimina dismissed_at — la UI vuelve a mostrar el panel con el mismo progreso.
| Método | Ruta | Descripción |
|---|---|---|
| GET | /onboarding/progress |
Estado actual para el usuario autenticado. Response: { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Usuario sin actividad → ceros/null sin crear fila. |
| POST | /onboarding/event |
Registrar transición de paso. Body: { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Validación por whitelist → 400 para step/status desconocido. Response: mismo shape que GET. Emite onboarding_step_completed/onboarding_step_skipped en activity_log solo al changed; al llegar a 5/5 emite adicionalmente onboarding_completed con duration_ms. |
| POST | /onboarding/dismiss |
Cerrar el panel (dismissed_at = now). Idempotente. Emite onboarding_dismissed en la primera llamada con payload {completed_count}. |
| POST | /onboarding/replay |
Volver a abrir el panel cerrado (dismissed_at = NULL). El estado de los pasos no se modifica. Emite onboarding_replayed al limpiar el evento. |
| POST | /projects/:name/active-issue |
Issue #115. Vincular la sesión web actual a un issue. Body: { issue_id: number, title?: string }. Escribe evento session_active_issue en activity_log (source=web). |
| GET | /projects/:name/active-issue |
Issue #115. Último issue vinculado de este owner en los últimos 7 días. Response: { active_issue_id, title, ts }. |
| GET | /onboarding/cli-status |
Phase 54.3 (issue #58). ¿El usuario inició sesión con arc login en los últimos 30 días? Response: { installed: boolean, last_cli_at: string|null }. SSOT — filas en activity_log con event_type='cli_invocation' y actor=chatId. El checklist de onboarding del frontend hace polling a este endpoint cada 10s mientras el paso CLI esté pendiente; cuando installed=true — marca automáticamente el paso cli como completado. |
| GET | /analytics/onboarding-funnel |
Phase 54.6 (issue #61). Estadísticas agregadas del funnel en ventana deslizante. Query: hours=168 (1-720, default 7d). Response: { hours, total_steps:5, started_users, completed_users, completion_rate, per_step: [{step, completed, skipped}…], duration_p50_ms, duration_p90_ms, ttfc_p50_ms, ttfc_sample_size }. SSOT — eventos de activity_log onboarding_step_* + onboarding_completed + cli_invocation. TTFC = time-to-first-arc (delta julianday desde el primer onboarding-step hasta el primer cli_invocation por actor). |
El SSOT para las métricas del funnel (Phase 54.6 / issue #61) son los eventos en activity_log (event_type LIKE 'onboarding_%'). La tabla onboarding_progress es un caché derivado: la UI se renderiza con una sola consulta en lugar de agregar eventos.
Beta Feedback (Phase 53.3)
| Método | Ruta | Descripción |
|---|---|---|
| POST | /feedback |
Enviar feedback beta. Body: {type: "bug"|"feature"|"other", title, description, project?, browser?}. Escribe en activity_log (event_type=feedback_report) y hace ping al CEO en Telegram. |
| GET | /admin/feedback |
Lista de submissions recientes (solo admin). Query: limit=50 (máx 500). Response: {items: [...], count}. |
POST /feedback — validación del body: type ∈ {bug,feature,other}, title ≤200 chars, description ≤5000 chars. Éxito → {ok: true, type, title}. El ping de Telegram tiene el formato 🐞/💡/📝 New <type> feedback ... From: <user> Title: <title> + primeros 400 caracteres de la descripción.
El widget flotante en
FeedbackWidget.jsx(CRM dashboard) envía automáticamentebrowser(UA + viewport + locale) yproject(technical_name del proyecto activo).
Beta Invites (Phase 52.1, solo admin)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /admin/invites |
Lista de todos los códigos de invitación + conteos (total_active, total_used). Solo admin. |
| POST | /admin/invites |
Generar N códigos. Body: {count: N, note?: string}. Solo admin. Response: {ok, codes, count}. |
| DELETE | /admin/invites/:code |
Revocar código de invitación no utilizado. |
Actualización del flujo de auth: POST /api/auth/register ahora requiere el campo invite_code (closed beta Phase 52.1). Sin código → 403 {error: "invite_required"}. Código inválido/usado → 403 {error: "invalid_invite"}.
Billing (Phase 51)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /billing/status |
Plan actual, límites, usage, features. Crea automáticamente la fila Free en la primera llamada. Response: { plan, status, current_period_end, limits, usage, features, pricing, can_upgrade, stripe_ready } |
| POST | /billing/checkout-session |
Stripe Checkout (Stage 2 — devuelve 501 hasta que el SDK de Stripe esté integrado) |
Límites del plan (semántica OR):
- Free: 1 proyecto AND 5 workers
- Min ($4.99/mo): 5 proyectos OR 25 workers en total
- Max ($11.99/mo): 20 proyectos OR 150 workers en total
Respuesta 402 en POST /onboarding/setup o POST /projects/:name/workers cuando se supera el límite: { error: "plan_limit_reached", reason: "projects_limit"|"workers_limit", current, limit, plan, message }
Los usuarios admin (
role=admin) omiten completamente la verificación de límites del plan — son operadores, no tenants de pago.
Los beta-testers (
subscriptions.plan='beta', Phase 52 F&F) también la omiten — proyectos y workers ilimitados más todas las features Max. Se asigna manualmente:UPDATE subscriptions SET plan='beta' WHERE user_id=?.
Bugfix (issue #25):
POST /projects/create(Quick Start, Phase 50.2) antes fallaba conownerChatId is not definedpor un typo — corregido, el actor del audit ahora se registra correctamente.
Bugfix (issue #26): allocatePort() para nuevos proyectos ahora prueba los bindings TCP reales (
ss -tln), no solo el registry. Antes podía devolver un puerto ocupado por un servicio fuera del registry (NotebookLM bridge :19213, internal bridges) → el workspace bot fallaba con EADDRINUSE.
Flujo de auth (Phase 50.1): /api/auth/register y /api/auth/login ahora devuelven JWT incluso para email no verificado + flag needs_verification: true. Las acciones sensibles (trial grant, billing, invites) verifican email_verified por separado. Rate limit en signup: 3 / IP / 24h.
Proyectos (9 endpoints)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /projects |
Lista de proyectos del usuario |
| POST | /projects/create |
Crear proyecto |
| GET | /projects/:name |
Detalles del proyecto |
| GET | /projects/:name/config |
Configuración del proyecto |
| PUT | /projects/:name/config |
Actualizar configuración |
| GET | /projects/:name/protocol |
Protocolo del proyecto |
| PUT | /projects/:name/protocol |
Actualizar protocolo |
| GET | /projects/:name/logs |
Logs del proyecto |
| GET | /projects/:name/metrics |
Métricas del proyecto |
POST /projects/create — body:
{
"technical_name": "string",
"displayName": "string",
"description": "string",
"icon": "string",
"color": "string"
}
GET /projects/:name/logs — query: category, lines
GET /projects/:name/metrics — query: since, until
Workers (11 endpoints)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /projects/:name/workers |
Lista de workers |
| POST | /projects/:name/workers |
Crear worker |
| POST | /projects/:name/workers/reorder |
Phase 53.8 — reordenar workers. Body: {order: [id1, id2, ...]}. Reescribe workers_registry.json de forma atómica. Los workers ausentes en order se añaden al final (protección contra pérdida de datos). Response: {ok, count, order}. |
| PUT | /projects/:name/workers/:id |
Actualizar worker |
| DELETE | /projects/:name/workers/:id |
Eliminar worker |
| POST | /projects/:name/workers/generate-prompt |
Generar prompt de sistema |
| GET | /projects/:name/workers/:id/telegram-token |
Obtener token de Telegram |
| POST | /projects/:name/workers/:id/telegram-token |
Phase 53.4 — valida el token con Telegram getMe, guarda bot_username en el vault, rechaza si el mismo bot ya está vinculado a otro worker (409). Response: {ok, started, bot_username}. |
| DELETE | /projects/:name/workers/:id/telegram-token |
Eliminar token de Telegram |
| POST | /projects/:name/workers/:id/notify |
Phase 53.2 — enviar ping de evento TG ({event?, text, buttons?}). No-op silencioso si no hay token vinculado o CRM_DISABLE_TG_NOTIFY=1. |
| POST | /projects/:name/workers/:id/suggest-bot-username |
53.11.1 (issue #48) — devuelve 5 candidatos de username TG para el wizard de creación de bot con formato <project>_<worker>_bot + fallbacks numerados. Slugify elimina guiones, trunca a 32 chars (la parte del worker se trunca primero). Response: {candidates: string[]}. |
| POST | /metrics/wizard |
53.11.1 (issue #48) — sink de telemetría para el wizard de creación de bot. Body: {action, duration_ms?, attempts?, success?, project?, worker_id?}. Escribe en activity_log (event_type=wizard_metric), best-effort. |
| GET | /analytics/wizard-metrics?hours=168 |
53.11.1 (issue #48) — resumen del funnel: {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Default 7 días, clamp 1-720h. |
| POST | /projects/:name/restart |
Reiniciar worker |
| GET | /projects/:name/active-role |
Rol activo actual |
| POST | /projects/:name/active-role |
Cambiar rol activo |
POST /projects/:name/workers — body:
{
"label": "string",
"icon": "string",
"type": "terminal | telegram",
"model": "string",
"max_turns": 20,
"tools": ["Read", "Write", "Bash"],
"system_prompt": "string",
"focus_dirs": ["src/", "docs/"]
}
max_turnspor defecto es20(antes era5, lo que causaba el error "Reached max turns" en diálogos multi-paso con tool calls).
POST /projects/:name/restart — query: worker_id
Archivos y almacenamiento (8 endpoints)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /projects/:name/files |
Árbol de archivos |
| POST | /projects/:name/files/upload |
Subir archivo (multipart, máx 100MB) |
| POST | /projects/:name/files/mkdir |
Crear directorio |
| POST | /projects/:name/files/create |
Crear archivo |
| GET | /projects/:name/files/read |
Leer archivo |
| PUT | /projects/:name/files/save |
Guardar archivo |
| DELETE | /projects/:name/files/delete |
Eliminar archivo |
| POST | /projects/:name/files/clone |
Git clone de repositorio |
GET /projects/:name/files — query: path
GET /projects/:name/files/read — query: path, raw
Skills (18 endpoints)
Skills del proyecto
| Método | Ruta | Descripción |
|---|---|---|
| GET | /projects/:name/skills |
Lista de skills del proyecto |
| POST | /projects/:name/skills |
Crear skill |
| PUT | /projects/:name/skills/:id |
Actualizar skill |
| DELETE | /projects/:name/skills/:id |
Eliminar skill |
Marketplace global
| Método | Ruta | Descripción |
|---|---|---|
| GET | /skills |
Lista de skills globales |
| POST | /skills |
Publicar skill |
| GET | /skills/:id |
Detalles de skill |
| PUT | /skills/:id |
Actualizar skill |
| DELETE | /skills/:id |
Eliminar skill |
Evolución y actualizaciones
| Método | Ruta | Descripción |
|---|---|---|
| GET | /skills/:id/evolution |
Historial de evolución de la skill |
| GET | /skill-updates |
Lista de actualizaciones disponibles |
| POST | /skill-updates/:id/approve |
Aceptar actualización |
| POST | /skill-updates/:id/reject |
Rechazar actualización |
Forks de skills
| Método | Ruta | Descripción |
|---|---|---|
| GET | /projects/:name/skill-forks |
Lista de forks |
| POST | /projects/:name/skill-forks |
Crear fork |
| PUT | /projects/:name/skill-forks/:id |
Actualizar fork |
| DELETE | /projects/:name/skill-forks/:id |
Eliminar fork |
Chat y mensajes
| Método | Ruta | Descripción |
|---|---|---|
| POST | /projects/:name/chat |
Enviar mensaje al chat |
| GET | /projects/:name/chat/history |
Historial de chat |
| POST | /projects/:name/message |
Enviar mensaje al worker (Phase 48.6: wake-up automático del worker inactivo, ~2-4s cold start; Phase 48.6.1: el wake-up ahora funciona también en proyectos single-mode, no solo parallel) |
| GET | /projects/:name/pins |
Lista de notas (pins) |
| POST | /projects/:name/pins |
Crear nota |
| DELETE | /projects/:name/pins/:id |
Eliminar nota |
Wiki (4 endpoints)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /projects/:name/wiki/tree |
Árbol de páginas wiki |
| GET | /projects/:name/wiki/file |
Leer página wiki |
| PUT | /projects/:name/wiki/save |
Guardar página wiki |
| GET | /projects/:name/wiki/download |
Descargar wiki como archivo ZIP |
Analytics (4 endpoints)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /analytics/activity |
Feed de actividad |
| GET | /analytics/sidebar |
Datos para el panel lateral |
| GET | /analytics/phases |
Lista de fases del proyecto |
| POST | /analytics/phases |
Actualizar fases del proyecto |
Marketplace y Sage (8 endpoints)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /sage/scout/categories |
Categorías del marketplace |
| POST | /sage/scout |
Buscar skills |
| POST | /sage/scout/quick-scan |
Escaneo rápido |
| POST | /sage/scout/analyze |
Análisis profundo de skill |
| POST | /sage/scout/install |
Instalar skill |
| POST | /sage/analyze |
Análisis de Sage |
| GET | /sage/status |
Estado del servicio Sage |
| POST | /sage/benchmark |
Ejecutar benchmark |
Memoria y Knowledge
| Método | Ruta | Descripción |
|---|---|---|
| POST | /projects/:name/memory/refresh |
Actualizar memoria neural |
| POST | /projects/:name/memory/fetch-artifact |
Descargar artefacto |
| GET | /projects/:name/learnings |
Lista de learnings |
| POST | /projects/:name/learnings |
Añadir learning |
| GET | /projects/:name/knowledge-graph |
Grafo de conocimiento del proyecto |
Documentación (global, sin auth)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /docs/tree?lang=<lang> |
Árbol de documentación; lang opcional (en/uk), default en |
| GET | /docs/file?path=<p>&lang=<lang> |
Leer archivo de documentación con language fallback |
GET /docs/tree — query: lang (opcional)
- Primero busca
docs/public/<lang>/index.md, fallback adocs/public/index.md - La respuesta incluye:
sections,files,served_lang,is_fallback,requested_lang
GET /docs/file — query: path (obligatorio), lang (opcional)
- Orden de resolución:
docs/public/<lang>/<path>→docs/public/<path>(fallback EN) - La respuesta incluye:
path,content,size,modified,served_lang,is_fallback,requested_lang - 403 en path traversal, 404 en archivo no encontrado
- Phase 52.1.3 — añadido parámetro
langpara traducción UK
Sistema
| Método | Ruta | Descripción |
|---|---|---|
| GET | /system/configs |
Obtener configuraciones del sistema |
| PUT | /system/configs |
Actualizar configuraciones del sistema |
Códigos de error
| Código | Significado |
|---|---|
| 200 | Éxito |
| 201 | Creado |
| 400 | Solicitud inválida |
| 401 | No autorizado |
| 403 | Prohibido (multi-tenancy) |
| 404 | No encontrado |
| 409 | Conflicto (duplicado) |
| 429 | Demasiadas solicitudes |
| 500 | Error del servidor |
GitHub Integration (Phase 49.3)
| Endpoint | Method | Descripción |
|---|---|---|
/api/crm/projects/:name/github |
GET | Lista de repos de GitHub vinculados al proyecto |
/api/crm/projects/:name/github |
POST | Vincular repo (body: {owner, repo}) — devuelve webhook URL + secret + instrucciones de configuración |
/api/crm/projects/:name/github/:id |
DELETE | Desvincular repo |
/api/crm/projects/:name/github/events |
GET | Lista de GitHub events recientes (Phase 49.3.1, query: ?limit=50) |
/api/webhooks/github |
POST | Receptor público de webhooks (validado con HMAC-SHA256, rate-limit 100/min) |
Eventos soportados: push, pull_request, workflow_run, issues. Las notificaciones se enrutan al Telegram del propietario del proyecto.
Account Security (Phase 45.4)
| Endpoint | Method | Descripción |
|---|---|---|
/api/crm/account/recovery |
GET | Lista de recovery keys activas |
/api/crm/account/recovery |
POST | Crear recovery key (body: encryptedKey, keyHint) |
/api/crm/account/recovery |
DELETE | Revocar recovery key(s) (body: { id } o {} para todas) |
/api/crm/account/recovery/restore |
GET | Obtener encrypted master key para recuperación |
Seguridad
- Multi-tenancy: cada endpoint
:nameverifica ownership mediantechatIddel JWT - Validación de nombre de proyecto:
^[a-zA-Z0-9][a-zA-Z0-9_-]*$(máx 64 caracteres) - Protección contra path traversal:
safePath()en todas las rutas controladas por el usuario - Subida de archivos: máx 100MB, extensiones bloqueadas (
.exe,.bat,.sh) - CORS: whitelist de origins mediante
CRM_ALLOWED_ORIGINS - Protección SSRF: allowlist en
handleScoutAnalyze— solo HTTPS + hosts permitidos - Endpoints internos: rechazan solicitudes con headers de proxy (
X-Forwarded-For,X-Real-IP) - Cifrado en reposo (Phase 45): las API keys y los mensajes de chat están cifrados con AES-256-GCM
- Headers de seguridad:
Content-Security-Policy,X-Frame-Options: DENY,X-Content-Type-Options: nosniff - Sanitización de PII: emails, API keys y JWTs se redactan automáticamente de los logs JSONL
Phase 53.13 — baseline de type-safety (2026-05-10)
Sin cambios de comportamiento en endpoints — solo tipos internos. tsc --noEmit ahora bloquea push/CI:
- La interfaz
ChildBotse consolidó enshared/routes/_utils.ts(3 duplicados unificados).bot_username,heartbeat_file,health_endpoint,statusse volvieron opcionales — reflejan el runtime-state (las entradas de workspace enriquecidas por DB frecuentemente carecen de ellos). requireAdmin()enshared/routes/system.tsahora devuelveResponse | { userId }en lugar de{ ok, ... }— narrowing más sencillo viainstanceof Response. El comportamiento externo (códigos 401/403, cuerpos de respuesta) no cambia.workers.tsDEFAULT_WORKERS perdióas const(para compatibilidad con callsites mutables); el parsing del body paratools/focus_dirsahora es estricto medianteArray.isArrayen lugar del fallback con||.
Phase 53.15 — Sentinel Sprint 1 (2026-05-10)
Cambios de comportamiento en endpoints de auth + admin (correcciones P0 del audit de Sentinel):
POST /api/auth/login— cuandorequires2fa=true, la respuesta ahora es{requires2fa: true, challenge_token}en lugar de{requires2fa: true, userId}. El frontend debe pasarchallenge_tokenen el siguiente paso.POST /api/auth/2fa/login— shape del body:{challenge_token, code}en lugar de{userId, code}. El token es de un solo uso, TTL de 5 min. Sin token válido el endpoint devuelve401 "Invalid or expired challenge — restart login". Rate-limit por userId de 5 intentos / 15 min → 429.POST /api/crm/skills+PUT /api/crm/skills/:id+DELETE /api/crm/skills/:id+POST /api/crm/skill-updates/:id/approve+POST /api/crm/skill-updates/:id/reject— solo admin. No-admin → 403Forbidden — admin only. Sin auth → 401.- Rate-limit de Nginx en
/api/auth/*— 5 req/min/IP (burst=10 nodelay → 429). Lo mismo en/api/webhooks/github(30 req/min/IP, burst=20). - HSTS — el header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadahora se envía en cada respuesta HTTPS. Las solicitudes HTTP → redirect 301 a HTTPS. X-Frame-Options: DENYen lugar deSAMEORIGIN.
Phase 53.21 — Sentinel P2 batch 2 (2026-05-12)
POST /api/crm/feedback— ahora requiere que el caller pueda acceder albody.projectindicado (verificación canAccessProject). Propietario no autorizado → 403"Project not accessible".projectvacío/ausente sigue siendo permitido (feedback global).POST /api/internal/trial/consume— shape del body cambiado:{project, owner_id, tokens}en lugar de{project, tokens}.owner_ides obligatorio, se verifica contraprojects.owner_iden DB. 404 para proyecto desconocido, 403 para owner mismatch. El caller (child-bot/claude-runner.ts) propagaARC_TRIAL_OWNERenv inyectado porworker-spawn.ts.
Phase 53.18 — corrección de filtración de secretos en tmux (2026-05-11)
Sin cambios de comportamiento en endpoints — solo refactor de rutas internas de spawn.
POST /api/crm/onboarding/setup(viashared/routes/onboarding.ts:startWorkspaceBot) — el método de arranque del child-bot en workspace-mode cambió debash -c "export X='val'; bun run bot.ts"atmux -e VAR=val ... bun run bot.ts. Los valores de tokens ya no aparecen en/proc/PID/cmdline. Externamente: 0 cambios (body de respuesta, status codes, comportamiento idéntico).
Phase 53.16 — Sentinel Sprint 2 (2026-05-10)
Cambios de comportamiento en endpoints tras el hardening de 13 × P1:
- OAuth callback — la URL de redirect usa fragmento
#token=en lugar de query?token=(Sentinel P1-8). El frontend lee desdewindow.location.hash(con fallback a?token=por un ciclo de deploy). /api/crm/analytics/activity+/api/crm/analytics/sidebar— la query ahora está limitada porowner_iddel usuario autenticado. Los no-admin solo ven sus propios proyectos. Antes se filtraban los primeros 80 chars de cada mensaje del asistente + nombres de proyecto + worker IDs de todos los tenants (Sentinel P1-4).PUT /api/crm/projects/:name/files/save— añadida verificaciónisProtectedPath()..env/CLAUDE.md/.git/*/.claude/*ahora devuelven 403"Protected path"(antes se podían sobreescribir) (Sentinel P1-3).POST /api/crm/projects/:name/files/mkdir+/files/create— body.name con..,.,/,\→ 400. Re-ejecución desafePath()trasjoin()(Sentinel P1-2)./ws/local-bridge— el chatId del JWT se conserva en el upgrade. El mensaje init conproject_nameque no pertenece al usuario → close 1008Forbidden — project not accessible. Antes cualquier usuario podía iniciar el bridge sobre un proyecto ajeno (Sentinel P1-5).- CSP — el HTML del frontend (via docker/nginx.conf) ahora envía CSP estricto:
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://arc-os.co wss://arc-os.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'. El CSP JSON de API perdió'unsafe-inline'(Sentinel P1-10). - Helper interno
extractChatId— ahora verifica la firma con verifyToken antes de decodificar (Sentinel P1-6, defensa en profundidad para futuras rutas skipAuth). - Formato cifrado de recovery key — las nuevas claves se guardan como
v2:<base64-salt>:<payload>(salt aleatorio de 16 bytes por clave). Las antiguas (sin prefijov2:) funcionan mediante fallback legado (Sentinel P1-13). - CEO_CHAT_ID — ahora es env-first (con fallback de advertencia a bot_registry). El hardcode 474903718 fue eliminado de 6 archivos (Sentinel P1-14).
- Nginx X-Forwarded-For — sobrescribir en lugar de añadir en los 17 callsites (Sentinel P1-11). El helper
clientIplee el ÚLTIMO segmento XFF (Sentinel P1-7).
Phase 55 — Cosmic Editorial login (2026-05-13)
Nuevos endpoints para inicio de sesión por magic-link:
POST /api/auth/magic-link/request— body{ email }. Genera un token de un solo uso con TTL de 10 min enephemeral_tokens(tipomagic_link), envía el enlacehttps://<host>/?magic_token=<token>a través del proveedor de email. Anti-enumeración: siempre 200 OK con body{ ok: true, message: "If the account exists, a magic link has been sent" }(incluso si el email no existe). Rate-limit: 3/min por (IP+email) + 5/10min por email — mismo contrato queforgot-password. La ruta de fallo aplica timing pad.POST /api/auth/magic-link/verify— body{ token }. Consume el token de un solo uso, devuelve{ ok: true, token: <jwt>, userId }en caso de éxito o 401"Invalid or expired magic link". Efecto secundario:user.email_verified = true+last_loginse actualiza (prueba de inbox = verificación).
El union EphemeralTokenType se extendió: ahora incluye "magic_link" junto a los existentes oauth_state / password_reset / email_verification / tfa_challenge.
El frontend (CosmicCard.jsx) gestiona el estado magic (cuenta regresiva de reenvío de 60s) y el parámetro URL ?magic_token= (auto-consume → login → animación de éxito).
Phase 56 — AI Interop / Project Context Export (2026-05-13)
Exportación exclusiva para owners de un snapshot saneado del proyecto como .md para transferirlo a una IA externa (Gemini / ChatGPT / Perplexity / Claude.ai).
GET /api/crm/projects/:name/context-export— params:include=section1,section2,...(secciones:identity / workers / architecture / issues / activity / commits / learnings; default = las 7),scanOnly=true|false,activityHours=N(1-720, default 168),commitLimit=N(1-200, default 20),issueStatus=open|closed|all. Solo owner — el rol admin NO hace bypass (por diseño). El bypass CEO funciona. Devuelve{ project, exportedAt, filename: "<project>-context-YYYY-MM-DD.md", scanOnly, sections, markdown, findings, stats, alertFired, preferences }. Redacta automáticamente los hallazgos críticos a menos quepreferences.auto_redact_critical = false. Las ejecuciones no-scanOnlyescriben enexport_audit_log.GET /api/crm/projects/:name/exports— lista de auditoría (solo owner). Params:limit=N(1-200, default 50). Devuelve{ project, exports: [{ id, owner_id, exported_at, sections[], findings_critical/high/medium/low, bytes }] }.GET /api/crm/projects/:name/settings/export— leer preferencias (solo owner). Devuelve{ project_name, always_include_emails, auto_redact_critical, notify_on_export, updated_at }.PATCH /api/crm/projects/:name/settings/export— actualizar preferencias (solo owner). El body acepta cualquier subconjunto de{ always_include_emails, auto_redact_critical, notify_on_export }(booleanos). Devuelve las preferencias actualizadas.GET /api/crm/analytics/exports— estadísticas agregadas (requiere auth, sin gate de owner — tarjeta de analytics). Param:hours=N(1-720, default 168). Devuelve{ total, byProject: [{ project_name, n, last }], severitySums: { critical, high, medium, low } }.
Alerta: cuando el owner supera 3 exportaciones en 24h Y prefs.notify_on_export = true (por defecto OFF) — logActivity("export_alert", ...) pasa por el pipeline de notificación TG de Phase 53.10 (alertFired: true en el body de la respuesta).
El scanner multi-nivel (shared/secret-scanner.ts) — Tier 1 regex (PATTERN_REGISTRY del sanitizador PII), Tier 2 entropía Shannon ≥4.5 bits/char en cadenas de ≥20 chars, Tier 3 heurísticas de contexto (key=/token:/secret=/password=). Whitelist: UUID / git SHA / SHA-256 / chars repetidos / hex corto / base58 de baja entropía. Niveles de severidad (critical/high/medium/low). Rendimiento: <500 ms / 1 MB.
Migración DB 024 — tablas export_audit_log + export_preferences.
Phase 57 — Platform Settings (seguimiento Sentinel #103, 2026-05-15)
Gestión de secretos super-admin desde la UI de CRM en lugar de ssh/editar-.env/pegar-en-chat. Backend MVP (Stage 1 de 4 stages). Todos los endpoints requieren requireAdmin (Phase 53.15) — devuelven 403 Forbidden — admin only para no-admin, 401 Unauthorized sin JWT.
GET /api/crm/platform/settings— devuelve{ items: [{ name, label, description, testable, restartTargets[], set, preview, length, lastRotated, lastRotatedBy }] }. Allowlist de 9 claves (ANTHROPIC_API_KEY,PLATFORM_ANTHROPIC_KEY,GITHUB_CLIENT_ID/SECRET,GOOGLE_CLIENT_ID/SECRET,MASTER_BOT_TOKEN,CITADEL_BOT_TOKEN,RESEND_API_KEY). Preview redactado:prefix(12)…suffix(4)+ longitud. El valor completo nunca sale del servidor.PUT /api/crm/platform/settings/:name— body{ value: string ≥ 8 chars }. Escribe atómicamente en el vault mediantestoreSecret(name, value)+ fila de auditoría. 400 si el nombre no está en el allowlist; 400 si value < 8 chars; 500 en fallo de escritura en vault.POST /api/crm/platform/settings/:name/test— verificar contra la API SaaS. Anthropic →GET /v1/modelsconx-api-key; TG →getMe; Resend →/api-keys. Los client secrets OAuth no son testables de forma independiente → 501. Devuelve{ ok: bool, reason?: string, detail?: string }. Timeout de 8s medianteAbortController.POST /api/crm/platform/settings/:name/restart—Bun.spawn(["nohup", "bash", "-c", "sleep 1 && tmux kill-session ... && bash start-*.sh"], { detach: true })sobre las sesiones tmux vinculadas. Desacoplado para que el restart del master no cancele la respuesta en vuelo. Devuelve{ ok: true, restarted: [sessions], note }.GET /api/crm/platform/audit?limit=50&key=ANTHROPIC_API_KEY— entradas recientes del log de auditoría, las más nuevas primero (límite máximo 500). Filtro por clave opcional.
Lista de exclusión fija NEVER_EXPOSE: CRM_SECRET (firma JWT) + SECRET_ENCRYPTION_KEY (meta-clave del vault) — incluso una solicitud admin con token válido devuelve 400 "not managed". El log de auditoría es append-only (sin handler UPDATE/DELETE), cada acción (incluidas las fallidas) escribe una fila con IP + UA + email.
Migración DB 026 — tabla platform_audit_log. Stage 2 (frontend PlatformSettings.jsx) — publicado el 2026-05-15 (cbc8bac): grid de tarjetas solo para admin + modal de rotación (<input type="password"> + confirmación de reescritura) + drawer de auditoría; entrada en sidebar filtrada por userRole === "admin" obtenido de /api/auth/me.
Polish (2026-05-15, commit 56191b0) — Restructura de la UI de Platform Settings. Los items de la respuesta de GET /api/crm/platform/settings ganan 5 nuevos campos: category (anthropic|oauth|telegram|email), usedIn (string[] — archivos/flujos que consumen la clave), getFromUrl (dónde obtener un valor nuevo), effectAfterRotate, riskIfLeaked. El frontend los utiliza para renderizar 4 grupos de tarjetas por sección + panel de ayuda colapsable por tarjeta con contexto estructurado (Used in / Get from / Effect / Risk). Sin cambios de comportamiento en los endpoints mutadores (PUT/POST/restart/test).
Refactor (2026-05-16) — cleanup interno de shared/routes/platform.ts. Se eliminaron 39 líneas (16 añadidas), sin cambios en la superficie pública de la API. Las firmas y respuestas de los endpoints PUT/POST/restart/test/audit no cambian. Documentado aquí solo porque el gate pre-push de cobertura de documentación se activa ante cualquier diff en shared/routes/*.ts.
Actividad backdated (#117, 2026-05-16) — POST /api/mcp/issues/:project/:id/log ahora acepta el campo opcional ts (string ISO-8601). Lo usa arc retro en la reconstrucción para que las entradas históricas queden en sus timestamps originales. Los valores con fecha futura se recortan silenciosamente a now dentro de addActivity() (defensa contra errores de tipeo). ISO inválido → 400.
Stage 3 (2026-05-15) — recarga en caliente de secretos OAuth + Resend sin reinicio. shared/auth.ts loadOAuthConfig() ahora lee getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") por llamada en lugar de process.env. Los callsites en master-bot/routes/auth.ts ya invocaban getOAuthConfig() por request → 0 cambios en callsites. RESEND_API_KEY ya tiene hot-reload mediante shared/email.ts:47. Cambio de comportamiento: PUT /api/crm/platform/settings/{GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GOOGLE_CLIENT_ID|GOOGLE_CLIENT_SECRET|RESEND_API_KEY} ahora surte efecto en la siguiente solicitud, sin necesidad de reinicio. restartTargets para estas 5 claves está vacío → el botón Restart en la UI está oculto. Caso límite: un flujo OAuth con state-token emitido antes de la rotación puede recibir un 400 en el callback durante el code-exchange — el usuario puede resolver reintentando. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN siguen requiriendo reinicio (se leen al hacer spawn del child-bot / inicio del long-poll TG).
Cleanup Phase 57.3.5 (2026-05-16) — allowlist MANAGED_KEYS reducida de 9 a 6. Eliminadas: ANTHROPIC_API_KEY (los operadores ahora usan PLATFORM_ANTHROPIC_KEY tanto para trial-credits como para inferencia de plataforma; el fallback a .env sigue funcionando para rutas de código legado hasta que Sage/Karpathy migren), CITADEL_BOT_TOKEN (el bot por proyecto pertenece a las entradas child:<name>:token del vault, gestionadas por el flujo de onboarding de workers — no en Platform Settings). MASTER_BOT_TOKEN reutilizado: label → "Telegram — System Monitor Bot", descripción → "Server health alerts + on-demand status probes (admin-only, not a chat bot)". La Phase 58 añadirá el loop de monitoreo (alertas push para crash de worker / disco / RAM / brute-force SSH / bypass CF + comandos /status, /health, /errors, /restart). Conjunto final: PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (refs #103).
Phase 63 — Consolidación UI/UX + Seguimiento de Uso de Tokens (2026-05-21, #148)
Nuevo endpoint:
POST /api/internal/usage/log(solo loopback) — escribe una fila entoken_usage_log. Body:{ project_name, owner_id, worker_id?, input_tokens, output_tokens, cache_tokens, total_tokens }. Llamado desdechild-bot/bot.tscomo fire-and-forget tras cada llamada a Claude (callClaudeOnce+callWorkertext path). No requiere header de auth —/api/internal/*solo es accesible desde localhost y bloqueado por nginx para requests externos.GET /api/crm/account/usage— historial de uso de tokens para el usuario autorizado (descrito en la tabla de Onboarding arriba).
Cambios en claude-runner.ts:
callClaudeOnce+callWorkertext path: ahora siempre--output-format json(antestextpara non-trial). El parse JSON extraeresultcomo texto de salida yusagepara logging. El flujo trial consume no cambia.- Nuevo dep
logUsage?enClaudeRunnerDeps— callback(workerId, { input, output, cache }) => void.
Cambios de UI (no API):
UserDropdown: componenteUsageCardcon total tokens + "Details →" al abrir; warning dot en avatar cuando el balance trial < 20%.BillingPage: sección Token Usage con barra de totales + tabla de 50 filas. Plan Enterprise (en desarrollo). Toggledetailsen cada tarjeta.OnboardingProgressPill: rediseñado como dropdown inline en el header (ya no es wizard modal).WorkerSelector: CSS vars semánticas--worker-{role}en lugar de tokens Tailwind chart.