API CRM — Référence des endpoints

Arc OS — Le Système d'orchestration pour équipes IA

Informations générales

Paramètre Valeur
Base URL https://arc-os.co/api/crm
Autorisation Authorization: Bearer <JWT> ou ?token=<JWT> (pour SSE/WebSocket)
Content-Type application/json
Algorithme JWT HMAC-SHA256
TTL du JWT 24 heures

Authentification

Tous les endpoints (sauf /docs/*) nécessitent un token JWT dans l'en-tête Authorization: Bearer <token>.

Pour les connexions SSE et WebSocket, le token est passé via le paramètre de requête ?token=<JWT>.

Erreurs d'autorisation

Code Description
401 Token manquant ou invalide
403 Pas d'accès au projet (multi-tenancy)

Endpoints par catégorie

Compte & paramètres

Méthode Chemin Description
GET /account/settings Récupérer les paramètres du compte
PUT /account/settings Mettre à jour les paramètres du compte

Onboarding + crédits d'essai (Phase 50.1)

Méthode Chemin Description
POST /onboarding/setup Crée le premier projet. Body multipart : config (JSON) + files. Le champ anthropicKey est désormais optionnel — s'il est vide + l'utilisateur a email_verified + n'a pas encore reçu d'essai, le projet est créé en trial_mode=1 avec 100K tokens gratuits. Réponse : { ok, project, trial_activated }. Phase 51 : renvoie 402 avec {error:"plan_limit_reached", reason, current, limit, plan} quand l'utilisateur a dépassé la limite de projets de son plan.
GET /account/trial-status Statut de l'essai pour la bannière de l'UI. Réponse : { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] }
GET /account/usage Historique d'utilisation des tokens pour l'utilisateur authentifié (Phase 63, #148). Réponse : { rows: [ { project_name, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at } × jusqu'à 200 ], totals: { total, input, output } }. Lit token_usage_log par owner_id. Affiché dans UserDropdown (UsageCard) et BillingPage (section Token Usage).
GET /account/billing-summary Résumé de facturation consolidé (#309). Réponse : { arc: { plan, status, tokens_this_month, tokens_input, tokens_output, renewal_date }, anthropic: { connected, key_prefix, credit_balance_usd, spend_month_usd, tokens_this_week } }. La section Anthropic est remplie côté serveur via l'API Anthropic en utilisant l'account_settings.anthropic_key de l'utilisateur (fallback → PLATFORM_ANTHROPIC_KEY). Affiché dans la UsageCard du UserDropdown.

Checklist d'onboarding (Phase 54.1, issue #56)

Checklist d'engagement en 5 étapes après l'assistant. Chaque étape (workers, cli, skill, bot, issue) accepte un statut completed ou skipped. Les mutations sont idempotentes : un POST identique répété renvoie le même état et n'écrit pas de doublon dans activity_log. Un replay ne réinitialise pas l'état, il ne fait qu'effacer dismissed_at — l'UI réaffiche le panneau avec la même progression.

Méthode Chemin Description
GET /onboarding/progress État courant pour l'utilisateur authentifié. Réponse : { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Utilisateur intact → zéros/null sans créer de ligne.
POST /onboarding/event Enregistre une transition d'étape. Body : { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Validation par whitelist → 400 sur une étape/statut inconnu. Réponse : même forme que GET. Émet onboarding_step_completed/onboarding_step_skipped vers activity_log uniquement quand changed ; lors de la transition vers 5/5, émet en plus onboarding_completed avec duration_ms.
POST /onboarding/dismiss Ferme le panneau (dismissed_at = now). Idempotent. Émet onboarding_dismissed au premier appel avec le payload {completed_count}.
POST /onboarding/replay Rouvre un panneau fermé (dismissed_at = NULL). L'état des étapes est inchangé. Émet onboarding_replayed lors d'un clear-event.
POST /onboarding/reset Reset complet limité à soi pour retester le flow nouvel utilisateur (#594) : efface l'onboarding_progress de l'appelant + les tours (user_tours/tour_events) + users.heard_source. Non destructif — les projets sont conservés. Émet onboarding_reset. Renvoie {ok:true}.
GET /onboarding/videos Liens vidéo d'onboarding (#603). Tout utilisateur authentifié (le frontend affiche des liens « ▶ Regarder »). Réponse : { videos: { <slot>: { url, seconds, title } } } pour les slots overview/plan/workers/issue/bot/cli/notes/bridge, fusionné par-dessus les valeurs par défaut. Stocké dans system_configs sous la clé ONBOARDING_VIDEOS.
PUT /onboarding/videos Admin uniquement (requireAdmin). Body : { videos: { <slot>: { url, seconds, title } } }. Persiste les slots connus (assainis) dans system_configs ; émet onboarding_videos_updated. Renvoie {ok:true, videos}. Pas de rebuild — les liens se mettent à jour en direct.
POST /projects/:name/active-issue Issue #115. Lie la session web courante à une issue. Body : { issue_id: number, title?: string }. Écrit l'événement activity_log session_active_issue (source=web).
GET /projects/:name/active-issue Issue #115. Dernière issue liée pour ce propriétaire sur 7 jours. Réponse : { active_issue_id, title, ts }.
GET /onboarding/cli-status Phase 54.3 (issue #58). L'utilisateur s'est-il connecté via arc login dans les 30 derniers jours ? Réponse : { installed: boolean, last_cli_at: string|null }. SSOT — lignes dans activity_log avec event_type='cli_invocation' et actor=chatId. La checklist d'onboarding du frontend interroge cet endpoint toutes les 10s tant que l'étape CLI est en attente ; quand installed=true — l'étape cli est automatiquement marquée comme terminée.
GET /cli/devices #627 (Phase B de #617). Les installations arc CLI liées à l'appelant, depuis cli_devices. Réponse : { devices: [{ device_id, hostname, platform, arc_version, last_project, first_seen, last_seen }] } (max 20, plus récentes d'abord). Alimenté par POST /api/cli/heartbeat — un check-in authentifié fire-and-forget que le CLI (≥1.0.14) envoie au login et au démarrage de chaque session de projet avec { device_id, hostname, platform, version, project? } ; le premier appareil par utilisateur logge aussi cli_invocation dans activity_log.
GET /projects/:name/cli-sessions #630 (Phase E de #617). Événements de début/fin de session CLI pour la page Sessions en lecture seule, depuis timeline_events (id LIKE 'cli-%', label LIKE 'CLI session%'). Query : limit (par défaut 120, max 500). Réponse : { events: [{ id, worker_id, label, timestamp, duration, metadata }], transcriptSessionIds: string[] } — le frontend apparie les starts/ends en lignes de session. #624 E.2 : la metadata de l'événement de fin porte le résumé riche { first_prompt, issue_id, issue_title, session_id, message_count } (écrit par arc ≥1.0.15 à la fin de session ; les sessions plus anciennes s'affichent en lignes simples). #632 : transcriptSessionIds liste (limité au propriétaire) quelles sessions ont un transcript uploadé → la page Sessions affiche un lien « Voir le transcript ». Limité au propriétaire via le contrôle d'accès projet standard.
POST /api/cli/session-end/:project/:mode #204 + #624 E.2. Appelé par le arc CLI quand une session se termine. Body : { duration_ms, first_prompt?, issue_id?, issue_title?, session_id?, message_count? }. Écrit un événement timeline CLI session ended ; les champs de résumé optionnels (best-effort, lus depuis le transcript local ~/.claude) atterrissent dans la metadata de l'événement pour la page Sessions. first_prompt est réduit des espaces + tronqué à 280 caractères côté serveur.
POST /api/cli/transcript/:project/:mode #632. Upload opt-in d'un transcript complet de session CLI pour la continuité cross-device. Auth : Bearer (propriétaire = sujet du token). Body : { session_id, device_id?, message_count?, first_prompt?, size_bytes?, gzip_b64 }gzip_b64 est le base64 du jsonl brut gzippé. Upsert cli_transcripts sur (owner, session_id). Payload compressé plafonné à 4 Mo (413 au-delà). Envoyé par arc ≥1.0.16 uniquement quand le flag upload_transcripts du compte est activé (lu depuis settings de /api/cli/init).
GET /projects/:name/cli-transcript?session=:id #632. Visualiseur de transcript limité au propriétaire. Décompresse le jsonl stocké et renvoie { session_id, worker_id, first_prompt, message_count, size_bytes, updated_at, messages: [{ role, text, ts }] } (texte user/assistant uniquement). 401 si non authentifié, 404 si aucun transcript pour ce propriétaire+projet+session.
GET /api/cli/transcripts/:project #634. Liste côté CLI des transcripts uploadés par le propriétaire pour un projet (métadonnées seulement : { transcripts: [{ session_id, worker_id, message_count, first_prompt, size_bytes, updated_at }] }). Limité au propriétaire automatiquement (le propriétaire doit avoir accès au projet). Alimente arc sessions <project> --from-server.
GET /api/cli/transcript/:project/:session #634. Téléchargement jsonl brut côté CLI pour réhydrater une session sur une autre machine. Renvoie { session_id, worker_id, message_count, size_bytes, updated_at, jsonl }jsonl est le transcript original dégzippé, que le CLI écrit dans ~/.claude/projects/<cwd>/<session>.jsonl pour que claude --resume puisse s'y rattacher. 404 si ce n'est pas celui du propriétaire.
GET /analytics/onboarding-funnel Phase 54.6 (issue #61). Admin uniquement (#497 — stats à l'échelle de la plateforme, 403 pour les utilisateurs normaux). Stats agrégées de funnel sur une fenêtre glissante. Query : hours=168 (1-720, par défaut 7 jours). Réponse : { 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 — événements activity_log onboarding_step_* + onboarding_completed + cli_invocation. TTFC = time-to-first-arc (delta julianday entre la première étape d'onboarding et la première cli_invocation par acteur).

SSOT pour les métriques de funnel (Phase 54.6 / issue #61) — événements dans activity_log (event_type LIKE 'onboarding_%'). La table onboarding_progress est un cache dérivé : l'UI s'affiche avec une seule requête au lieu d'agréger sur les événements.

| GET | /analytics/lifecycle-funnel | #519 Part A. Admin uniquement. Funnel de cycle de vie sur une cohorte d'inscription : signup → email_verified → first_project → first_worker → first_message → first_response → return_day2. Query : days=30 (1-365). Réponse : { windowDays, cohortSize, steps: [{key, count, pctOfCohort, pctOfPrev, medianSecondsFromPrev}…], segments: {web|tg|cli: {cohort, steps}}, acceptance: {medianSignupToFirstResponseSec, targetSec:120, sampleSize} }. Sources : users (signup/verified), projects.owner_id (premier projet), activity_log worker_created (+ fallback project_created seeded par preset), chat_messages via jointure owner (message/réponse), auth_events login ≥24h après le signup (day-2). Segments : cli = preuve device_code_approve/cli_invocation ; tg = project_channels telegram ou id numérique né sur TG ; sinon web. | | GET | /analytics/response-latency | #560. Admin uniquement. Latence du pipeline de première réponse du worker, par étape, depuis chat_messages.metadata.latency écrit par le child bot pour les réponses issues du CRM. Query : days=7 (1-90). Réponse : { windowDays, sampleSize, stages: {queue_ms|prep_ms|gen_ms|total_ms: {p50, p90, n}} }. Étapes : queue = POST→sortie de l'inbox ; prep = dequeue→spawn de claude ; gen = temps mur de claude ; total = POST→réponse persistée. Les percentiles sont en nearest-rank. | | GET | /analytics/cascade | #562 S5. Admin uniquement. Télémétrie de cascade de modèles gated par spec. Query : days=14 (1-90). Réponse : { windowDays, applied, escalated, escalationReasons: {class: n}, byModel: [{model, turns, total_tokens}] }. Sources : événements activity_log cascade_applied/cascade_escalated + token_usage_log.model (migration 064). Les classes de raison regroupent le préfixe avant : (ex. eval_failure). |

Feedback bêta (Phase 53.3)

Méthode Chemin Description
POST /feedback Soumettre un feedback bêta. Body : {type: "bug"|"feature"|"other", title, description, project?, browser?}. Écrit dans activity_log (event_type=feedback_report) et ping le CEO sur Telegram.
GET /admin/feedback Liste les soumissions récentes (admin uniquement). Query : limit=50 (max 500). Réponse : {items: [...], count}.
POST /feedback/translation Soumettre un problème de traduction (Phase 59.4). Body : {locale, msgid, suggestion, severity: "minor"|"major"|"wrong", current_translation?, page_url?}. Stocke dans translation_feedback.
GET /admin/translations Liste le feedback de traduction (admin). Query : locale, status=open|accepted|rejected|all, limit. Réponse : {items, count}.
GET /admin/translations/stats Stats de santé par locale (admin). Réponse : {stats: [{locale, total, open_count, accepted, rejected, critical_open}]}.
POST /admin/translations/:id/accept Accepter une suggestion — patche le fichier .po sur le disque. Body : {note?}. Réponse : {ok, po_patched, glossary_suggestion}.
POST /admin/translations/:id/reject Rejeter une suggestion. Body : {note?}. Réponse : {ok}.

POST /feedback/translation — valide : locale ∈ {uk,de,es,fr,pl,pt-BR,ru}, msgid ≤1000, suggestion ≤2000, severity ∈ {minor,major,wrong}. Après 3+ suggestions acceptées pour le même msgid → glossary_suggestion: true dans la réponse d'acceptation.

Le widget flottant dans FeedbackWidget.jsx a désormais un 4e type, « Translation » — remplit automatiquement la locale depuis i18n.locale, capture msgid + suggestion + severity.

Chat IA Arc Help (Phase 61, #147)

Méthode Chemin Description
POST /help/chat Q&R IA in-app. Body : {message, history: [{role,text}]}. Réponse : {reply, sources: string[], remaining, limit}. Rate limit : 30/jour/utilisateur.
GET /help/usage Limite courante. Réponse : {remaining, limit, used}.

POST /help/chat — pipeline : (1) vérification du rate-limit (429 si dépassé), (2) RAG via shared/rag.ts (Cohere + sqlite-vec, Phase 71) fusionnant les hits skill du projet + _global_ → fallback recherche par mots-clés dans docs/public/, (3) Claude Haiku avec system prompt + contexte doc + historique. message ≤2000 caractères. Répond dans la langue de la requête.

Invitations bêta (Phase 52.1, admin uniquement)

Méthode Chemin Description
GET /admin/dashboard Dashboard système (Phase 60.9, #145). Admin uniquement. Renvoie : CPU/RAM/Disque depuis /proc, utilisateurs par plan, flotte de conteneurs, 50 derniers événements d'activité, stats waitlist + projet + issue.
GET /admin/wipe-metrics Dashboard de télémétrie WIP-E (#308). Admin uniquement. Renvoie : {render: {count, avg_ms, p50_ms, p95_ms, max_ms}, interaction: {count, avg_per_session, p95_per_session, max_per_session, sessions_zero}, by_worker: [{worker_id, render_count, avg_render_ms, session_count, avg_interactions}], daily: [{date, renders, interactions, avg_render_ms}], recent: [...]}.
GET /admin/waitlist Liste toutes les candidatures de la waitlist. Admin uniquement. Réponse : {entries: [{id, email, message, status, created_at}]}.
POST /admin/waitlist/:id/approve Approuver une candidature — génère un code d'invitation (arc-XXXX-XXXX), envoie un email avec le code, met le statut à approved. Réponse : {ok, invite_code, email_sent}.
POST /admin/waitlist/:id/reject Rejeter une candidature. Réponse : {ok}.
GET /admin/invites Liste tous les codes d'invitation + compteurs (total_active, total_used). Admin uniquement.
POST /admin/invites Génère N codes. Body : {count: N, note?: string}. Admin uniquement. Réponse : {ok, codes, count}.
DELETE /admin/invites/:code Révoquer un code d'invitation non utilisé.
/admin/notebooklm/* Supprimé en Phase 71.8 avec le bridge NotebookLM. La recherche sémantique passe désormais par le RAG auto-hébergé (rag-architecture.md).

Mise à jour du flux d'auth : POST /api/auth/register nécessite désormais un champ invite_code (bêta fermée Phase 52.1). Sans code → 403 {error: "invite_required"}. Code invalide/déjà utilisé → 403 {error: "invalid_invite"}.

Standard Cloud — Terminal WebSocket + Logs SSE (Phase 60 #139)

Protocole Chemin Description
WS /ws/cloud/:userId/terminal?token=<JWT> Proxy vers docker exec -i <containerId> /bin/bash. IDOR : userId doit correspondre au chatId du JWT. Un conteneur en pause est automatiquement réveillé. Trames WS entrantes → stdin du conteneur ; stdout+stderr → trames WS.
SSE /api/sse/cloud/:userId/logs docker logs -f --tail 50 pour le conteneur de l'utilisateur. Auth : Bearer JWT. IDOR : userId === chatId. Événements : data: {"line": "..."} par ligne, data: {"closed": true} à la sortie.
SSE /api/sse/cli-status?token=<JWT> #639 (T3). Flux « en attente de ton terminal… » de l'onboarding. Interroge le signal cli_invocation de l'appelant (~1,5s) et émet event: linked {"linked":true} dès que le CLI se lie, puis ferme ; : keep-alive en commentaire pour le heartbeat ; event: end {"reason":"timeout"} après 10 min. L'assistant l'ouvre via EventSource et garde son poll fetch cli-status de 3s en fallback. Limité au propriétaire via le sujet du token.

Note de télémétrie (#640, T4) : GET /api/cli/download/:platform écrit désormais une ligne cli_download dans activity_log en fire-and-forget ({platform}, acteur best-effort quand un token est présent) — ouvre le funnel téléchargement→login affiché dans la carte admin « CLI Activation & TTV ».

Note de télémétrie (#641, T5) : POST /projects/:name/message (envoi du chat navigateur) écrit un événement d'activité browser_chat_message en fire-and-forget — le signal d'usage derrière la décision « basculer/retirer le chat navigateur », affiché (utilisateurs chat navigateur vs CLI) dans la carte admin « CLI Activation & TTV ».

Standard Cloud (Phase 60)

Méthode Chemin Description
POST /cloud/claude-verify Vérifie claude --version dans le conteneur (shell-quoted via SSH en mode remote-host, sûr pour le transport, #329). Met claude_authed=true. Réponse : { ok, output }
POST /cloud/ssh-keygen Génère une clé ed25519 dans le conteneur (idempotent). Réponse : { public_key }
POST /cloud/ssh-verify ssh -T [email protected] dans le conteneur. Met github_authed=true en cas de succès. Réponse : { ok, output }
POST /cloud/provision Provisionner un conteneur Docker pour l'utilisateur. Nécessite le plan cloud, 402 sinon. Idempotent : si un conteneur existe déjà — renvoie l'état courant. Réponse : { container_id, status, server_ip, port, claude_authed, github_authed }
GET /cloud/status État du conteneur + réconciliation live docker inspect. Réponse : { container_id, status, server_ip, internal_port, claude_authed, github_authed, docker_running, last_active, created_at } ou { status: "none" }
POST /cloud/deprovision Arrêter + supprimer le conteneur (docker stop + docker rm -f + docker network rm arc-net-{id}). Met status=deleted dans la DB. Réponse : { ok: true, container_id }

Statuts du conteneur : provisioningreadypausedsuspended / deleted.

Sécurité (SEC-60 #152, #154, #155, #156) : chaque conteneur est isolé dans son propre réseau arc-net-{id} (prévention du mouvement latéral). La connexion SSH Contabo→Hetzner utilise un utilisateur dédié arcapi (groupe docker, pas de root) avec un wrapper docker-only — les commandes non-docker sont bloquées au niveau authorized_keys. ARC_TOKEN est injecté via docker exec après démarrage (non visible dans docker inspect). git clone est contraint par timeout 60. Timeout d'inactivité WebSocket : 120s. docker logs SSE est limité à --since 1h. Prévention IDOR : tous les endpoints vérifient container.user_id === req.userId. Flags de sécurité sur docker run : --cap-drop=ALL --security-opt=no-new-privileges --cpus=1.5 --memory=2g --pids-limit=200. Volumes : arc-{id}-workspace:/workspace, arc-{id}-claude:/home/arcuser/.claude, arc-{id}-ssh:/home/arcuser/.ssh. Cycle de vie (#141) : GET /cloud/status met toujours à jour last_active. Inactif 30 min → docker pause (cron toutes les 5 min, scripts/cloud-lifecycle-cron.ts). Réveil : message CRM, message TG, upgrade WS → docker unpause automatiquement.

Waitlist (#134) :

Méthode Chemin Description
POST /cloud/waitlist Rejoindre la file. Idempotent. Réponse : { position, status, joined_at, message }. 409 si déjà sur le plan cloud ou si un conteneur existe déjà.
GET /cloud/waitlist/status Son propre statut dans la file. Réponse : { position, status, joined_at, invited_at } ou { status: "not_joined" }.
GET /cloud/waitlist Admin uniquement. Liste complète + stats. Réponse : { stats: { total, waiting, invited, activated }, list: [...] }.
POST /cloud/waitlist/invite Admin uniquement. Inviter un utilisateur. Body : { user_id }. Met status=invited + passe automatiquement le plan à cloud. Réponse : { ok, user_id, position }.

Facturation (Phase 51 → #202 Plata by mono)

Phase #202 : Stripe remplacé par Plata by mono (acquiring internet monobank). Abonnements récurrents via tokenisation (la carte est enregistrée au premier paiement).

Méthode Chemin Description
GET /billing/status Plan courant, limites, usage, fonctionnalités. Réponse : { plan, status, current_period_end, next_billing_date, plata_masked_pan, limits, usage, features, pricing, can_upgrade, plata_ready }
POST /billing/checkout-session Crée une facture Plata avec tokenisation. Body : { plan: "min"|"cloud", success_url?, cancel_url? }. Réponse : { url, invoice_id, plan, amount_uah }. 503 si PLATA_MERCHANT_TOKEN n'est pas dans le vault.
POST /billing/webhook Callback Plata (PAS d'auth CRM — vérifié par l'en-tête X-Token). Statuts : success (active le plan + stocke cardToken), failure/expired (incrémente billing_failures, 3+ → downgrade vers free). Idempotent via la table plata_events.
POST /billing/cancel Annuler l'abonnement (downgrade vers free). Met en pause le conteneur Docker pour le plan cloud. Réponse : { ok, plan: "free" }.

#205 (2026-05-26) : L'ancienne route /billing/portal-session a été supprimée avec le code mort Stripe. Utilise /billing/cancel pour annuler un abonnement.

Limites de plan (sémantique OR) :

Réponse 402 sur POST /onboarding/setup ou POST /projects/:name/workers quand la limite est dépassée : { error: "plan_limit_reached", reason: "projects_limit"|"workers_limit", current, limit, plan, message }

Les utilisateurs admin (role=admin) contournent entièrement le contrôle de limite de plan — ce sont des opérateurs, pas des locataires payants.

Les bêta-testeurs (subscriptions.plan='beta', Phase 52 F&F) le contournent aussi — projets/workers illimités plus toutes les fonctionnalités Max. Assigné manuellement : UPDATE subscriptions SET plan='beta' WHERE user_id=?.

Correctif de bug (issue #25) : POST /projects/create (Quick Start, Phase 50.2) plantait auparavant avec ownerChatId is not defined à cause d'une faute de frappe — corrigé, l'acteur d'audit est maintenant enregistré correctement.

Correctif de bug (issue #26) : allocatePort() pour les nouveaux projets sonde désormais les vrais bindings TCP (ss -tln), pas seulement le registry. Auparavant il pouvait attribuer un port occupé par un service hors-registry (bridge NotebookLM :19213, bridges internes) → le bot workspace plantait avec EADDRINUSE.

Flux d'auth (Phase 50.1) : /api/auth/register et /api/auth/login renvoient désormais un JWT même pour un email non vérifié + le flag needs_verification: true. Les actions sensibles (octroi d'essai, facturation, invitations) vérifient email_verified séparément. Rate limit à l'inscription : 3 / IP / 24h.


Projets (9 endpoints)

Méthode Chemin Description
GET /projects Liste les projets de l'utilisateur
POST /projects/create Crée un projet — body : {displayName, projectName, niche?, teamPreset?} ; pour les utilisateurs d'essai, met automatiquement trial_mode=1 et injecte PLATFORM_ANTHROPIC_KEY
POST /projects/create-with-team Création atomique de projet + workers + (optionnel) bot TG en une seule requête — body : {project, workers[], telegram?} ; rollback en cas d'erreur. #517 : telegram.token тепер зберігається у project:<name>:telegram:bot_token (unified channel bots, Phase 76) — раніше писався у legacy child:<name>:token, який unified-боти не читають.
GET /projects/suggest-preset Suggestion de preset par niche — query : niche=<text> ; renvoie {preset_id} basé sur une table de mots-clés
GET /projects/:name Détails du projet
GET /projects/:name/config Configuration du projet
PUT /projects/:name/config Mettre à jour la configuration
POST /projects/:name/upload-icon Uploader une icône de projet PNG/GIF
POST /projects/:name/workers/:id/upload-icon Uploader une icône de worker PNG/GIF
GET /projects/:name/protocol Protocole du projet
PUT /projects/:name/protocol Mettre à jour le protocole
GET /projects/:name/logs Logs du projet
GET /projects/:name/metrics Métriques du projet

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éthode Chemin Description
GET /workers #304 Phase A — tous les workers de tous les projets de l'utilisateur courant. Réponse : { workers: [{ id, label, icon, type, model, tools, context_assets, project_name }] }. Filtré par owner_id (multi-tenancy). Le CEO voit tous les projets.
GET /workers/presets #228 — bibliothèque globale de presets (indépendante du projet). Renvoie 13 workers depuis le canonique config/workers_registry.json : { presets: [{ id, label, icon, type, model, max_turns, tools, system_prompt, context_assets, focus_dirs, prompt_style }] }. Utilisé par WorkerCreationWizard à l'étape 1.
GET /model-tiers #518 — config de tiering des modèles depuis config/model_tiers.json. Renvoie { tiers: [{ key, label, stage, model, hint }], roleDefaults: { <roleId>: <tierKey> }, fallbackTier }. Alimente le sélecteur de modèle par message du composer (labels d'étape Auto + Opus/Sonnet/Haiku) et le défaut rôle→tier appliqué à la création de worker. #520 la résolution au spawn respecte le model_mode par projet : override model par message > (strict → modèle configuré par worker · optimizedefaultModelForRole(role)). Le mode se bascule via PUT /projects/:name/config { modelMode: "strict"|"optimize" } (par défaut strict).
GET /workers/templates #304 Phase I — templates de l'utilisateur courant. Réponse : { templates: [{ id, name, description, config, is_public, created_at }] }.
POST /workers/templates #304 Phase I — enregistrer/mettre à jour un template. Body : { name, description?, config }. Réponse : { ok, id }.
DELETE /workers/templates/:id #304 Phase I — supprimer un template (propriétaire uniquement). Réponse : { ok }.
GET /projects/:name/workers Liste les workers
POST /projects/:name/workers Crée un worker
POST /projects/:name/workers/reorder Phase 53.8 — réordonne les workers. Body : {order: [id1, id2, ...]}. Réécrit atomiquement workers_registry.json. Les workers absents de order sont ajoutés à la fin (protection contre la perte). Réponse : {ok, count, order}.
PUT /projects/:name/workers/:id Mettre à jour un worker
DELETE /projects/:name/workers/:id Supprimer un worker
POST /projects/:name/workers/generate-prompt Générer un system prompt
GET /projects/:name/workers/:id/telegram-token Récupérer le token Telegram
POST /projects/:name/workers/:id/telegram-token Phase 53.4 — valide le token via Telegram getMe, stocke bot_username dans le vault, refuse si le même bot est déjà lié à un autre worker (409). Réponse : {ok, started, bot_username}.
DELETE /projects/:name/workers/:id/telegram-token Supprimer le token Telegram
POST /projects/:name/workers/:id/avatar #304 Phase D — uploader un avatar (multipart file, JPEG/PNG/WebP, max 2 Mo). Vérification des magic bytes. Stocke dans data/worker-avatars/, écrit dans worker_avatars (migration 043). Réponse : { ok, url }.
GET /projects/:name/workers/:id/avatar #304 Phase D — récupère l'avatar en binaire (Content-Type correspondant au MIME). 404 si aucun avatar n'est uploadé.
DELETE /projects/:name/workers/:id/avatar #304 Phase D — supprime l'avatar, remet avatar_pack='role' dans le JSON du worker.
GET /projects/:name/workers/:id/activity #306 — feed d'activité du worker (50 derniers événements). Fusionné : activity_log (actor=workerId) + project_issues.activity (author=workerId) + token_usage_log (snapshots quotidiens). Réponse : { events: [{ type, title, detail, when }] }. Types : git_commit, skill_loaded, skill_unloaded, issue_pick, issue_close, issue_log, token_budget, session_start.
GET /projects/:name/workers/:id/runtime #306 — état runtime du worker. Réponse : { status: 'working'|'idle', status_started_at, tokens_today, tokens_pct, tokens_cap, current_skill }. Lit d'abord workers_runtime_state (migration 045) ; fallback de péremption : status='working' + tmux mort + updated_at > 10 min → idle (détection de crash). Plafond quotidien selon le plan via lookup subscriptions.plan : free=100K, starter=400K, starter_cloud=2M, beta=non compté (renvoie tokens_cap: null, tokens_pct: 0). Intervalle de poll 15s.
POST /projects/:name/workers/:id/notify Phase 53.2 — envoie un ping d'événement TG ({event?, text, buttons?}). No-op silencieux si aucun token n'est lié ou si CRM_DISABLE_TG_NOTIFY=1.
POST /projects/:name/workers/:id/suggest-bot-username 53.11.1 (issue #48) — renvoie 5 candidats de username TG pour l'assistant de création de bot au format <project>_<worker>_bot + fallbacks numérotés. Slugify retire les traits d'union, tronque à 32 caractères (la partie worker est rognée en premier). Réponse : {candidates: string[]}.
POST /metrics/wizard 53.11.1 (issue #48) — puits de télémétrie pour l'assistant de création de bot. Body : {action, duration_ms?, attempts?, success?, project?, worker_id?, locale?} (#124 : événements locale_active/locale_switch). Écrit dans activity_log (event_type=wizard_metric), best-effort.
GET /analytics/wizard-metrics?hours=168 53.11.1 (issue #48) — résumé de funnel admin uniquement (#497) : {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Par défaut 7 jours, borné 1-720h.
POST /projects/:name/restart Redémarrer un worker
GET /projects/:name/active-role Rôle actif courant
POST /projects/:name/active-role Changer le rôle actif

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/"],
  "auto_approve": false
}

max_turns vaut 20 par défaut (auparavant 5, ce qui provoquait l'erreur « Reached max turns » dans les dialogues multi-étapes avec appels d'outils).

auto_approve (#596, par défaut false) — quand true, le spawn Claude du worker reçoit --permission-mode acceptEdits (session arc CLI interactive, /api/cli/init renvoie le flag, et les invocations claude -p du child-bot), pour que le worker ne s'arrête pas pour confirmer les éditions de fichiers. Bash et d'autres actions demandent toujours. Accepté aussi par PUT /projects/:name/workers/:id.

POST /projects/:name/restart — query : worker_id


Fichiers & stockage (8 endpoints)

Méthode Chemin Description
GET /projects/:name/files Arborescence de fichiers
POST /projects/:name/files/upload Uploader un fichier (multipart, max 100 Mo)
POST /projects/:name/files/mkdir Créer un répertoire
POST /projects/:name/files/create Créer un fichier
GET /projects/:name/files/read Lire un fichier
PUT /projects/:name/files/save Enregistrer un fichier
DELETE /projects/:name/files/delete Supprimer un fichier
POST /projects/:name/files/clone Git clone d'un dépôt

GET /projects/:name/files — query : path

GET /projects/:name/files/read — query : path, raw


Skills (18 endpoints)

Skills de projet

Méthode Chemin Description
GET /projects/:name/skills Liste les skills du projet. Renvoie les skills globaux (owner_project=NULL) + les skills de ce projet (owner_project=name). Les skills d'autres projets ne sont pas inclus (#157).
POST /projects/:name/skills Crée un skill. Stocké avec owner_project=name, visible seulement par ce projet.
PUT /projects/:name/skills/:id Mettre à jour un skill
DELETE /projects/:name/skills/:id Supprimer un skill

#210 (2026-05-26) : La DB (skills_global) est désormais le writer SSOT. Les sauvegardes de l'UI vont d'abord en DB ; .claude/skills/<name>/SKILL.md est écrit en write-through comme artefact pour que Claude Code CLI découvre automatiquement les skills. Les écritures legacy skills/<name>.md ont été supprimées — les fichiers existants ne sont plus lus ni maintenus. Helper de migration : scripts/migrate-skills-to-db.ts.

Marketplace global

Méthode Chemin Description
GET /skills Liste les skills globaux
POST /skills Publier un skill
GET /skills/:id Détails d'un skill
PUT /skills/:id Mettre à jour un skill
DELETE /skills/:id Supprimer un skill

Évolution & mises à jour

Méthode Chemin Description
GET /skills/:id/evolution Historique d'évolution d'un skill
GET /skill-updates Liste les mises à jour disponibles
POST /skill-updates/:id/approve Approuver une mise à jour
POST /skill-updates/:id/reject Rejeter une mise à jour

Forks de skills

Méthode Chemin Description
GET /projects/:name/skill-forks Liste les forks
POST /projects/:name/skill-forks Créer un fork
PUT /projects/:name/skill-forks/:id Mettre à jour un fork
DELETE /projects/:name/skill-forks/:id Supprimer un fork

Chat & messages

Méthode Chemin Description
POST /projects/:name/chat Envoyer un message au chat
GET /projects/:name/chat/history Historique du chat
POST /projects/:name/message Envoyer un message à un worker (Phase 48.6 : réveille automatiquement un worker tué par inactivité, cold start ~2-4s ; Phase 48.6.1 : le réveil fonctionne aussi dans les projets single-mode, pas seulement en parallèle)
GET /projects/:name/pins Liste les notes (pins)
POST /projects/:name/pins Créer une note
DELETE /projects/:name/pins/:id Supprimer une note

Wiki (4 endpoints)

Méthode Chemin Description
GET /projects/:name/wiki/tree Arborescence des pages du wiki
GET /projects/:name/wiki/file Lire une page de wiki
PUT /projects/:name/wiki/save Enregistrer une page de wiki. Phase 71.5 : déclenche syncWiki → re-embed via Cohere (fire-and-forget ; les échecs sont loggés, l'écriture ne échoue pas).
GET /projects/:name/wiki/download Télécharger le wiki en archive ZIP

Analytics (4 endpoints)

Méthode Chemin Description
GET /analytics/activity Feed d'activité
GET /api/crm/activity-feed #638 (T2). Colonne vertébrale Home/Activity — feed chronologique inverse, limité au propriétaire, sur tous les projets de l'utilisateur, fusionnant activity_log (types d'événement bruyants filtrés) avec github_events (commits/PRs). Query : limit (par défaut 40, max 100), before (curseur ISO). Réponse : { items: [{ src, id, project_name, actor, event_type, title, metadata, ts }], nextCursor }. Portée propriétaire = propriétaire du projet OU actor = user. Alimente la page Home.
GET /api/crm/activity-feed/unread?since=:iso #644 (T8). Nombre d'événements curés du feed Home plus récents que since (même portée propriétaire + filtre de bruit que /activity-feed), plafonné à 99. Alimente la cloche de notification de l'en-tête. Réponse : { count }.
GET /api/cli/download/:platform #300 — téléchargement de binaire non authentifié (linux-x64 / darwin-arm64 / darwin-x64 / windows-x64) servi depuis dist/arc-<platform> ; 404 avec hint si non compilé. Enveloppé par https://arc-os.co/install.sh + install.ps1 (statiques, frontend/public). Handler : master-bot/routes/cli.ts.
POST /sage/mcp/add #531 — додає Smithery MCP-сервер у .mcp.json проєкту ({projectName,namespace}{type:http,url:mcp.smithery.run/<ns>}, зберігає інші сервери). Потребує canAccessProject + наявний cwd. Підхоплюється воркером при наступному spawn.
GET /sage/scout/sources #529 — список доступних джерел discovery ({id,label}): claudemarketplaces (HTML), anthropic (official marketplace.json). POST /sage/scout приймає sources:[id] (порожньо=всі), fan-out + dedup by repo+path, stale прапорець.
GET /team-presets #517 — команди-бандли з config/team-presets.json (повні конфіги воркерів: model, system_prompt, tools). Живить Composer wizard; /workers/presets — то окремі ролі.
GET /models #575 — курований список моделей для всіх дропдаунів вибору моделі воркера. Гібрид: концертні версії резолвяться LIVE з Anthropic /v1/models (newest-per-tier, 1h cache), курація (tiers/labels/hints) — єдине рукотворне місце; FALLBACK якщо API недоступний. Returns { models: [{id,label,hint,tier,recommended}], live }. Пресети зберігають tier-аліаси ('sonnet'/'opus'), що резолвляться при створенні воркера.
GET /analytics/overview #497 S4 — дані для Dashboard-карток, owner-scoped: { tokens: {week, prev_week}, issues_by_priority: {P0..P3}, per_project: [{name,p0,p1,open_total}], last_activity: [{name,ts}] }. Tokens з token_usage_log (7д vs попередні 7д), issues зі статусами open/in_progress/blocked.
GET /analytics/sidebar Données pour la sidebar. #497 : hotProjects compte activity_log + chat_messages sur 24h (auparavant chat uniquement — affichait « no activity » à côté d'événements frais).
GET /analytics/phases Liste les phases du projet
POST /analytics/phases Mettre à jour les phases du projet

Marketplace & Sage (8 endpoints)

Méthode Chemin Description
GET /sage/scout/categories Catégories du marketplace
POST /sage/scout Rechercher des skills
POST /sage/scout/quick-scan Scan rapide
POST /sage/scout/analyze Analyse approfondie de skill
POST /sage/scout/install Installer un skill
POST /sage/analyze Analyse Sage
GET /sage/status Statut du service Sage
POST /sage/benchmark Lancer un benchmark

Memory & connaissances

Méthode Chemin Description
GET /projects/:name/rag/search?q=...&k=6&include_global=true&doc_types=wiki,issue,skill,transcript Phase 71.7 (#364) : recherche sémantique sur embeddings + embeddings_vec (Cohere + sqlite-vec). Paramètres : q (texte de requête), k (1-25, par défaut 6), include_global (par défaut true — fusionne avec le namespace skill _global_), doc_types (sous-ensemble séparé par virgules ; Phase 73.6 type additionnel : transcript). Réponse : `{ query, project, hits: [{rank, doc_type, doc_id, chunk_ix, distance, scope: 'project'
POST /projects/:name/memory/refresh Phase 71.8 (#365) : re-embed MANIFEST + ROADMAP + fichiers clés dans le store RAG (auparavant — sync vers NotebookLM). Même endpoint, nouvelle sémantique.
POST /projects/:name/memory/fetch-artifact Supprimé en Phase 71.8 (l'audio overview n'a pas d'équivalent RAG) — renvoie 410 Gone.
GET /projects/:name/learnings Liste les learnings
POST /projects/:name/learnings Ajouter un learning
GET /projects/:name/knowledge-graph Knowledge graph du projet

Documentation (globale, sans auth)

Méthode Chemin Description
GET /docs/tree?lang=<lang> Arborescence de la documentation ; lang est optionnel (en/uk), par défaut en
GET /docs/file?path=<p>&lang=<lang> Lire un fichier de documentation avec fallback de langue

GET /docs/tree — query : lang (optionnel)

GET /docs/file — query : path (requis), lang (optionnel)


Système

Méthode Chemin Description
GET /system/configs Récupérer les configurations système
PUT /system/configs Mettre à jour les configurations système

Codes d'erreur

Code Signification
200 Succès
201 Créé
400 Requête invalide
401 Non autorisé
403 Interdit (multi-tenancy)
404 Introuvable
409 Conflit (doublon)
429 Trop de requêtes
500 Erreur serveur

Intégration GitHub (Phase 49.3)

Endpoint Méthode Description
/api/crm/projects/:name/github GET Liste les repos GitHub liés au projet
/api/crm/projects/:name/github POST Lie un repo (body : {owner, repo}) — renvoie l'URL du webhook + le secret + les instructions de configuration
/api/crm/projects/:name/github/:id DELETE Délie un repo
/api/crm/projects/:name/github/events GET Liste les événements GitHub récents (Phase 49.3.1, query : ?limit=50)
/api/webhooks/github POST Récepteur de webhook public (validé HMAC-SHA256, rate-limit 100/min)

Événements supportés : push, pull_request, workflow_run, issues. Notifications routées vers le Telegram du propriétaire du projet.

Connecteurs CRM (#521)

Intégration CRM par projet (un connecteur par projet : RemOnline ou Odoo). Le worker claude reçoit les outils de lecture mcp__<provider>__* du connecteur ; les écritures sont mises en file pour approbation du propriétaire (jamais exécutées directement). Les identifiants sont stockés chiffrés dans le vault et ne sont jamais renvoyés au client. La couche connecteur est pluggable par provider, et les connecteurs individuels peuvent être gated par compte.

Endpoint Méthode Description
/api/crm/projects/:name/crm GET Provider courant + quels jeux d'identifiants sont configurés
/api/crm/projects/:name/crm PUT Définir le provider + enregistrer les identifiants dans le vault. Hot-reload du connecteur en respawnant le child (réponse : { ok, provider, reloaded }). Body : { provider: "remonline"|"odoo"|"none", remonline?: { api_key }, odoo?: { url, db, login, api_key } }
/api/crm/projects/:name/crm/test POST Tester une connexion (identifiants stockés ou fournis). Sonde en lecture seule (RemOnline GET /contacts/people ; Odoo JSON-RPC authenticate)
/api/crm/projects/:name/crm-writes GET Liste les demandes d'écriture en file/décidées pour le projet
/api/crm/projects/:name/crm-writes/:id/approve POST Approuver → exécute l'écriture contre le CRM avec les identifiants du vault
/api/crm/projects/:name/crm-writes/:id/reject POST Rejeter une écriture en file

Les identifiants vivent uniquement dans le vault (project:<name>:<provider>:*) et sont injectés dans le worker via env au spawn — jamais renvoyés au client ni placés dans argv. L'auth RemOnline est un Authorization: Bearer <api_key> direct (RO App API v2, https://api.roapp.io) ; l'auth Odoo est JSON-RPC common.authenticate.

Sécurité du compte (Phase 45.4)

Endpoint Méthode Description
/api/crm/account/recovery GET Liste les clés de récupération actives
/api/crm/account/recovery POST Crée une clé de récupération (body : encryptedKey, keyHint)
/api/crm/account/recovery DELETE Révoque une/des clé(s) de récupération (body : { id } ou {} pour toutes)
/api/crm/account/recovery/restore GET Récupère la clé maître chiffrée pour la récupération

Sécurité


Phase 53.13 — référence de type-safety (2026-05-10)

Aucun changement de comportement des endpoints — types internes uniquement. tsc --noEmit bloque désormais push/CI :


Remédiation du pentest Sentinel (2026-06-10, #433–#444)

Sprint de pentest white-box — changements de comportement des endpoints après correction de 3×P1 + 4×P2 + 3×P3 :


Phase 53.15 — Sentinel Sprint 1 (2026-05-10)

Changements de comportement pour les endpoints auth + admin (corrections P0 de l'audit Sentinel) :


Phase 53.21 — Sentinel P2 batch 2 (2026-05-12)

Phase 63 — Consolidation UI/UX + suivi d'utilisation des tokens (2026-05-21, #148)

Nouvel endpoint :

Changements dans claude-runner.ts :

Changements UI (pas API) :

Phase 53.18 — correctif de fuite de secret tmux (2026-05-11)

Aucun changement de comportement des endpoints — seulement un refactor des chemins de spawn internes.

Phase 53.16 — Sentinel Sprint 2 (2026-05-10)

Changements de comportement des endpoints après durcissement de 13 × P1 :

Phase 55 — Login Cosmic Editorial (2026-05-13)

Nouveaux endpoints pour la connexion par magic-link :

L'union EphemeralTokenType a été étendue : elle contient désormais "magic_link" aux côtés des oauth_state / password_reset / email_verification / tfa_challenge existants.

Le frontend (CosmicCard.jsx) gère l'état magic (compte à rebours de renvoi de 60 s) et le paramètre d'URL ?magic_token= (auto-consommation → login → animation de succès).

Phase 56 — AI Interop / Export du contexte de projet (2026-05-13)

Export réservé au propriétaire d'un instantané de projet assaini au format .md, pour le remettre à une IA externe (Gemini / ChatGPT / Perplexity / Claude.ai).

Alerte : quand un propriétaire dépasse 3 exports en 24h ET que prefs.notify_on_export = true (par défaut OFF) — logActivity("export_alert", ...) passe par le pipeline de notify TG Phase 53.10 existant (alertFired: true dans le corps de réponse).

Scanner multi-tier (shared/secret-scanner.ts) — Tier 1 regex (PATTERN_REGISTRY de l'assainisseur PII), Tier 2 entropie de Shannon ≥4,5 bits/char sur des runs de ≥20 caractères, Tier 3 heuristiques contextuelles (key=/token:/secret=/password=). Whitelist : UUID / git SHA / SHA-256 / caractères répétés / hex court / base58 à faible entropie. Tiers de sévérité (critical/high/medium/low). Performance : <500 ms / 1 Mo.

Migration DB 024 — tables export_audit_log + export_preferences.


Phase 57 — Platform Settings (suivi Sentinel #103, 2026-05-15)

Gestion des secrets super-admin via l'UI du CRM au lieu de ssh/édition-.env/collage-dans-le-chat. Backend MVP (étape 1 sur 4). Tous les endpoints sont gated par requireAdmin (Phase 53.15) — ils renvoient 403 Forbidden — admin only pour les non-admins, 401 Unauthorized sans JWT.

Liste d'exclusion stricte NEVER_EXPOSE : CRM_SECRET (signature JWT) + SECRET_ENCRYPTION_KEY (clé meta du vault) — même une requête admin avec un token valide renvoie 400 « not managed ». Le log d'audit est append-only (aucun handler UPDATE/DELETE) ; chaque action (y compris celles échouées) écrit une ligne avec IP + UA + email.

Migration DB 026 — table platform_audit_log. Étape 2 (frontend PlatformSettings.jsx) — livrée le 2026-05-15 (cbc8bac) : grille de cartes admin uniquement + modale de rotation (<input type="password"> + confirmation par re-saisie) + tiroir d'audit ; entrée sidebar filtrée par userRole === "admin" récupéré depuis /api/auth/me.

Polish (2026-05-15, commit 56191b0) — restructuration de l'UI Platform Settings. Les items de la réponse GET /api/crm/platform/settings gagnent 5 nouveaux champs : category (anthropic|oauth|telegram|email), usedIn (string[] — fichiers/flows qui consomment la clé), getFromUrl (où récupérer une nouvelle valeur), effectAfterRotate, riskIfLeaked. Utilisé par le frontend pour afficher 4 groupes de cartes sectionnés + un panneau d'aide repliable par carte avec un contexte structuré (Used in / Get from / Effect / Risk). Aucun changement de comportement des endpoints de mutation (PUT/POST/restart/test).

Refactor (2026-05-16) — nettoyage interne de shared/routes/platform.ts. 39 lignes retirées (16 ajoutées), aucun changement de surface d'API publique. Signatures et réponses des endpoints PUT/POST/restart/test/audit inchangées. Documenté ici uniquement parce que la gate de doc-coverage pre-push se déclenche sur tout diff shared/routes/*.ts.

Activité antidatée (#117, 2026-05-16)POST /api/mcp/issues/:project/:id/log accepte désormais un champ optionnel ts (chaîne ISO-8601). Utilisé par la reconstruction arc retro pour que les entrées historiques atterrissent à leurs horodatages d'origine. Les valeurs futures sont silencieusement ramenées à now dans addActivity() (défense contre les antidates fautives). ISO invalide → 400.

Étape 3 (2026-05-15) — hot-reload des secrets OAuth + Resend sans redémarrage. shared/auth.ts loadOAuthConfig() lit désormais getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") par appel au lieu de process.env. Les callsites dans master-bot/routes/auth.ts appelaient déjà getOAuthConfig() par requête → 0 changement de callsite. RESEND_API_KEY était déjà hot-reloadé via shared/email.ts:47. Changement de comportement : PUT /api/crm/platform/settings/{GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GOOGLE_CLIENT_ID|GOOGLE_CLIENT_SECRET|RESEND_API_KEY} prend effet dès la requête suivante, aucun redémarrage requis. restartTargets pour ces 5 clés est vide → le bouton Restart de l'UI est masqué. Cas limite : un flow OAuth avec un token de state émis avant la rotation peut recevoir un 400 au callback pendant l'échange de code — un retry de l'utilisateur le résout. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN restent restart-required (lus au spawn du child-bot / à l'init du long-poll TG).

Nettoyage Phase 57.3.5 (2026-05-16) — allowlist MANAGED_KEYS réduite de 9 → 6. Retirés : ANTHROPIC_API_KEY (les opérateurs utilisent désormais la seule PLATFORM_ANTHROPIC_KEY pour les crédits d'essai et l'inférence de plateforme ; le fallback .env fonctionne encore pour les chemins de code legacy jusqu'à ce que Sage/Karpathy migrent), CITADEL_BOT_TOKEN (le bot par projet appartient aux entrées vault child:<name>:token, gérées par le flow d'onboarding worker — pas Platform Settings). MASTER_BOT_TOKEN réaffecté : label → « Telegram — System Monitor Bot », description → « Alertes de santé serveur + sondes de statut à la demande (admin uniquement, pas un bot de chat) ». La Phase 58 ajoutera la boucle de monitoring (alertes push pour crash de worker / disque / RAM / brute-force SSH / bypass CF + commandes /status, /health, /errors, /restart). Jeu final : PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (réf #103).

Arc Help (Phase 61 / #147)

Historique (Phase 61 / #153) :

GDPR / Conformité (Sprint 1+2, #161–#174, 2026-05-22)

Droit à l'effacement — DELETE /api/auth/account (#162)

Supprime définitivement l'utilisateur authentifié et toutes ses données (GDPR Art. 17).

Version de mot de passe / invalidation de token (#174)

La migration 035 ajoute password_version INTEGER NOT NULL DEFAULT 0 à users. Au changement de mot de passe, password_version est incrémenté. Le payload JWT inclut le champ pv. crmAuthMiddleware valide pv contre la DB à chaque requête, rejetant les tokens émis avant le dernier changement de mot de passe (401 "Token invalidated — please log in again"). Fail open si la DB est indisponible.

Cron de rétention des données (#168)

Le master bot exécute une purge quotidienne au démarrage + toutes les 24h. Limites de rétention : chat_messages 180 jours (par timestamp), activity_log 365 jours (par created_at), auth_events 90 jours (par ts), token_usage_log 730 jours (par created_at unixepoch), export_audit_log 365 jours (par exported_at). Non fatal — l'effacement ne bloque pas le démarrage.

Conformité email (#167)

Tous les emails transactionnels sortants (reset de mot de passe, vérification, magic-link) incluent désormais :

Sécurité — vérification des mots de passe compromis HIBP (#171)

Sur POST /api/auth/register et POST /api/auth/reset-password, le mot de passe soumis est vérifié contre l'API k-anonymity de HaveIBeenPwned avant stockage. Seuls les 5 premiers caractères hex du hash SHA-1 sont envoyés à HIBP — le mot de passe complet ne quitte jamais le serveur. Si le mot de passe apparaît dans une base de fuites avec un count > 0, la requête est rejetée avec HTTP 400 : "This password was found in a known data breach. Please choose a different password." Fail open en cas de timeout/erreur HIBP (timeout de 4s) — un HIBP down ne bloque pas l'inscription.

Portabilité des données — GET /api/auth/export (#163)

GDPR Art. 20 — Droit à la portabilité des données. Renvoie un fichier JSON structuré contenant toutes les données personnelles qu'Arc OS détient sur l'utilisateur authentifié.

Arc Help — System prompt durci + anti-injection (#151)

Changements de comportement de POST /api/crm/help/chat (aucun changement de surface d'API) :

Durcissement de la discipline des workers (#187, #188, #189, 2026-05-23)

Expansion du statut des issues (#187)

PUT /api/mcp/issues/:project/:id accepte désormais des valeurs de statut étendues :

Statut Signification
open Pas encore commencé
in_progress En cours de travail actif (défini par arc issue take)
blocked En attente d'une dépendance externe
deferred Reporté (auparavant stocké comme texte uniquement)
closed Terminé

Nouveau champ assignee : les issues ont désormais assignee: string | null. Défini via arc issue take <id> ou --assignee <worker_id> dans arc issue update.

Migration 036 : ALTER TABLE project_issues ADD COLUMN assignee TEXT (nullable, auto-appliquée au démarrage du serveur).

Commande CLI arc issue take <id> (#187)

Raccourci pour revendiquer une issue : met assignee = current_worker_id, status = in_progress, logge l'activité, écrit l'état de session. Équivalent à :

arc issue update <id> --status in_progress --assignee developer
arc issue log <id> "Taken by developer — status set to in_progress"

Validation du hook commit-msg (#187)

.githooks/commit-msg valide désormais les issues #N référencées contre le issues/issues.json local :

Injection du PROJECT_MANIFEST.md dans le bridge (#188)

handleCliInit (shared/cli-routes.ts) lit désormais PROJECT_MANIFEST.md depuis la racine du projet et l'injecte dans le bloc CITADEL sous ## Project Context. Limite : 8000 caractères. Cela donne aux workers bridge (tournant sur les machines des clients via arc) l'accès à une architecture compacte, des patterns de sécurité, la structure des fichiers et des learnings clés du CLAUDE.md complet.

Emplacement : après PROJECT_RULES.md, avant la liste des skills.

Champ de config worker context_assets (#189)

La config worker dans workers_registry.json supporte un context_assets: string[] optionnel — liste de noms de skills qui sont automatiquement injectés dans chaque session bridge de ce worker (sans nécessiter arc skill <name>) :

{
  "id": "developer",
  "context_assets": ["crm-api-reference", "archivist_system"]
}

Le contenu de chaque skill est injecté sous ### Auto-Loaded Skills → #### Skill: <name>, tronqué à 3000 caractères chacun.

Phase 62 — Entrée vocale (#373, 2026-06-05)

Transcription vocale en temps réel proxifiée via le serveur whisper.cpp auto-hébergé (arc-whisper.service, port 19214, modèle ggml-base préchargé).

POST /api/crm/voice/transcribe (#373, Phase 62.4)

Transcrit de courts clips vocaux (dictée de chat). Proxifie l'audio vers le whisper-server local et renvoie le texte.

Auth : token Bearer (ou query ?token=).

Body : multipart/form-data

Champ Type Notes
audio Blob webm / ogg / wav. Max 25 Mo.
locale string BCP-47, ex. uk-UA, en-US. Passé comme param language à whisper.

Réponse 200 :

{ "transcript": "Що ти зробив вчора?" }

Codes d'erreur :

Code Signification
400 Champ audio ou locale manquant
413 Audio de plus de 25 Mo
429 Quota quotidien atteint (60 min/utilisateur/jour) OU serveur occupé (max 2 transcriptions simultanées)
502 whisper-server a renvoyé un non-200
500 Échec inattendu

Rate limit : voice_usage_log (migration 051) suit les secondes approximatives par (utilisateur, jour) en utilisant la taille en octets de l'upload comme proxy (suppose un codec voix ~32 kbps, précision ±30 %). Plafond strict : 3600 s / jour. Les requêtes qui dépasseraient le plafond renvoient 429 avant d'être transférées à whisper.

Note d'architecture : whisper tourne uniquement sur Contabo (pas dans les conteneurs Hetzner par utilisateur). Les octets audio ne quittent jamais Contabo ; le texte résultant est ce que voit le routage cloud-chat de la Phase 70. arc-whisper.service garde le modèle ggml-base préchargé pour que le coût par appel soit pur inference (~3,4 s à chaud pour 11 s d'audio, 3,1× temps réel sur la box EPYC 6-vCPU actuelle).


Phase 73 — Transcription de réunions + analyse (#377-#384, 2026-06-05)

Uploade l'audio/vidéo d'une réunion vers un projet, obtiens une transcription whisper + un résumé Claude, optionnellement embarqué dans le RAG. Toutes les routes sont gated par canAccessProject (propriétaire ou admin).

POST /api/crm/projects/:name/transcripts/upload (#377, Phase 73.1)

Upload multipart, renvoie 202 avec transcript_id + job_id + status:'queued'. Le job est pris en charge par la file in-process (max 1 simultané).

Champs du body :

Limites : upload max 1 Go, allow-list MIME (mp3/wav/m4a/aac/ogg/opus/flac + mp4/mov/webm/mkv).

Erreurs : 400 (champ manquant / mauvais MIME), 401, 413 (au-delà du plafond), 500 (écriture disque).

GET /api/crm/projects/:name/transcripts (#379, Phase 73.3)

Liste les transcripts du projet, paginé par curseur. Query : ?limit=20&cursor=<id>. Renvoie {items: TranscriptSummary[], next_cursor: number|null}.

GET /api/crm/projects/:name/transcripts/:id (#379)

Ligne complète incluant transcript_text, summary_json (parsé en objet), et frames_json (parsé quand la Phase 73.4 sera livrée).

GET /api/crm/projects/:name/transcripts/job/:jobId/progress (#379)

Flux SSE de la progression du job. Pousse event: progress avec {status, progress_pct, step_label, error} dès qu'un champ change, plus des heartbeats en commentaire : keep-alive toutes les 1s pour que l'idleTimeout de 10s de Bun ne tue pas les longs runs whisper. Ferme avec event: end une fois le statut terminal.

Auth : l'EventSource du navigateur ajoute ?token=<bearer> (ne peut pas définir l'en-tête Authorization).

Statuts terminaux : done (embed RAG post-Phase 73.6 + nettoyage du fichier), failed. Note : summarized est une étape transitoire — le SSE reste ouvert à travers embeddingdone. Le bouton d'envoi du frontend s'active à summarized (n'attend pas le RAG).

Machine à états (Phases 73.1-73.6)

queued
  → extracting_audio    (ffmpeg → WAV mono 16 kHz)
  → transcribing        (whisper-cli -t 4)
  → (video) extracting_frames → frames_extracted   (ffmpeg scene-change)
            → vision_analyzing → vision_analyzed   (Phase 73.4 Claude vision par frame)
  → (audio) transcribed
  → summarizing         (Claude Sonnet → summary_json, reçoit les frames vision en contexte)
  → summarized
  → embedding           (Phase 73.6 Cohere upsert via shared/rag.ts, sauté si embed_to_rag=0)
  → done                (fichier source + dossier de frames supprimés — décision CEO D4)

Forme du JSON des frames vision (Phase 73.4, #380)

Stocké comme chaîne JSON dans transcripts.frames_json (reparsé en objet par GET /transcripts/:id).

[
  { "ts_ms": 3000, "description": "Slide titled 'Q3 Revenue' with bar chart showing 30% growth." },
  { "ts_ms": 6000, "description": "Architecture diagram with three boxes labeled API/Worker/DB." }
]

Plafond strict MAX_FRAMES=50 par transcript (~0,15 $ dans le pire cas aux tarifs vision Sonnet typiques). Les frames au-delà du plafond sont silencieusement abandonnées, la dernière description conservée reçoit un suffixe [+N more frames dropped]. Les échecs par frame deviennent des chaînes [vision failed: <msg>] — ils n'interrompent pas la passe. Les frames décrites comme "No informational content" sont webcam-only ou décoratives.

Forme du JSON de résumé (Phase 73.5, #381)

Stocké comme chaîne JSON dans transcripts.summary_json. Reparsé en objet par GET /transcripts/:id.

{
  "tldr": "1-2 sentence executive summary",
  "key_points": ["..."],
  "action_items": [{"task": "...", "owner": "name or null"}],
  "decisions": ["..."],
  "topics": ["..."],
  "model": "claude-sonnet-4-5",
  "generated_at": "2026-06-05T20:04:15.573Z"
}

La résolution de la clé Anthropic reflète shared/worker-spawn.ts : BYOK account_settings.anthropic_key (déchiffré si chiffré), fallback PLATFORM_ANTHROPIC_KEY pour les propriétaires en mode essai. Les échecs de résumé sont non fatals — transcript_text reste intact, le statut revient à transcribed/frames_extracted pour que l'utilisateur puisse réessayer après avoir corrigé sa clé.

Phase 78 — Notes : Knowledge Collections (#394–#404, 2026-06-08)

Notes par projet de type NotebookLM. Chaque note est une collection de sources (vidéo, audio, YouTube, web, PDF, DOCX, TXT, image) avec un index RAG partagé et un chat.

GET /api/crm/projects/:name/notes

Renvoie toutes les notes du projet. Auth requise + canAccessProject.

Réponse 200 :

[{ "id": 1, "title": "Sprint planning", "description": null, "created_at": "...", "source_count": 3 }]

POST /api/crm/projects/:name/notes

Crée une nouvelle note.

Body : { "title": "string", "description": "string?" }
Réponse 201 : { "id": 1, "title": "Sprint planning" }

GET /api/crm/projects/:name/notes/:id

Récupère le détail d'une note avec sources, liens d'issues et historique de chat.

Réponse 200 :

{
  "id": 1, "title": "Sprint planning",
  "sources": [{ "id": 1, "source_type": "youtube", "title": "My video", "url": "...", "status": "done", "duration_seconds": 3600 }],
  "issue_links": [{ "issue_id": 42, "title": "Issue title" }],
  "chats": [{ "role": "user", "content": "Summarize", "created_at": "..." }]
}

DELETE /api/crm/projects/:name/notes/:id

Supprime la note et toutes les sources/chats. Cascade vers note_sources, note_chats, note_issue_links.

POST /api/crm/projects/:name/notes/:id/sources

Ajoute une source (upload de fichier, URL ou fichier de projet).

Content-Type : multipart/form-data OU application/json

Réponse 201 : { "source_id": 5, "status": "queued" }

Le traitement est asynchrone. Interroge GET /notes/:id jusqu'à ce que source.status === "done".

POST /api/crm/projects/:name/notes/:id/sources/chunk-init

Démarre un upload de fichier par chunks (#547). Cloudflare rejette les corps de requête au-delà de ~100 Mo, donc les fichiers au-dessus sont envoyés en chunks ; l'UI web bascule automatiquement à 90 Mo.

Body : { "filename": "meeting.mp4", "mime": "video/mp4", "size": 262144000 }
Réponse 201 : { "upload_id": "<uuid>", "chunk_size": 20971520 }

Le type et les limites de taille par type sont validés en amont (mêmes règles que l'upload direct). Les sessions expirent après 30 minutes d'inactivité ; max 10 sessions simultanées (429 au-delà).

POST /api/crm/projects/:name/notes/:id/sources/chunks/:uploadId

Ajoute un chunk à une session d'upload.

Content-Type : application/octet-stream — octets bruts du chunk (≤ chunk_size)
En-tête : X-Chunk-Index — base 0, strictement séquentiel (409 en cas de mismatch)

Réponse 200 (intermédiaire) : { "received_bytes": 41943040 }
Réponse 201 (dernier chunk, quand les octets reçus == la taille déclarée) : { "done": true, "source_id": 5, "status": "queued" } — la source suit alors le chemin de traitement normal.

PATCH /api/crm/projects/:name/notes/:id/sources/:sourceId

Renomme une source (édition inline du titre).

Body : { "title": "New name" }
Réponse 200 : {}
Passe une chaîne vide ou null pour revenir au nom de fichier/URL par défaut.

DELETE /api/crm/projects/:name/notes/:id/sources/:sourceId

Retire une source et son contenu.

GET /api/crm/projects/:name/notes/:id/sources/:sourceId/progress

Flux SSE de la progression du traitement de la source.

Événements : progress { "status": "processing"|"done"|"error", "message": "..." }

POST /api/crm/projects/:name/notes/:id/chat

Envoie un message au chat de la note. Réponse en flux SSE.

Body :

{
  "message": "Summarize all sources",
  "selectedSourceIds": [1, 3]
}

selectedSourceIds est optionnel — omets-le pour inclure toutes les sources.

Événements SSE :

Stratégie RAG : recherche sqlite-vec sur les embeddings de note_source → fallback injection directe de content_text (80 K caractères max) quand la recherche vectorielle est indisponible ou sans résultat. Un garde-fou anti-hallucination est injecté dans le system prompt quand des sources non traitées sont incluses.

Tool use — create_issue : Claude peut créer des issues de projet depuis le chat. Multi-turn : le turn 1 streame jusqu'à l'appel d'outil, le backend exécute (issueQueries.nextId + issueQueries.insert), le turn 2 reprend le streaming avec le résultat de l'outil injecté.

Machine à états du statut de source

queued → processing → done
                    ↘ error

Valeurs du champ status de la source :

Stratégie de transcript YouTube (Phase 78.3)

  1. youtube-transcript npm : cascade de langues ["en", "en-US", "en-GB"] → fallback n'importe laquelle
  2. API Supadata.ai : GET https://api.supadata.ai/v1/youtube/transcript?url=...&text=true&lang=en → fallback sans le param lang
  3. yt-dlp + Whisper : fallback final pour les vidéos sans sous-titres

Priorité : préférer les sous-titres anglais pour éviter les transcripts auto-traduits en arabe/autre langue.