CRM API — Référence des endpoints
Arc OS — The Orchestration System for AI Teams
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 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 transmis via le query param ?token=<JWT>.
Erreurs d'autorisation
| Code | Description |
|---|---|
| 401 | Token absent ou invalide |
| 403 | Accès au projet refusé (multi-tenancy) |
Endpoints par catégorie
Compte et 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 + Trial Credits (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 — si vide + user avec email_verified + n'ayant jamais eu d'essai gratuit, le projet est créé en trial_mode=1 avec 100K free tokens. Response : { ok, project, trial_activated }. Phase 51 : retourne 402 avec {error:"plan_limit_reached", reason, current, limit, plan} quand l'utilisateur dépasse la limite de projets pour son plan. |
| GET | /account/trial-status |
Statut de l'essai gratuit pour le bandeau UI. Response : { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] } |
Onboarding Checklist (Phase 54.1, issue #56)
Checklist d'engagement post-wizard en 5 étapes. Chaque étape (workers, cli, skill, bot, issue) accepte le statut completed ou skipped. Les mutations sont idempotentes : un POST identique répété retourne le même état, sans écrire de doublon dans activity_log. Replay ne réinitialise pas l'état, il efface seulement dismissed_at — l'UI réaffiche le panneau avec le même progression.
| Méthode | Chemin | Description |
|---|---|---|
| GET | /onboarding/progress |
État actuel pour l'utilisateur authentifié. Response : { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Utilisateur non touché → zéros/null sans création de ligne. |
| POST | /onboarding/event |
Enregistre une transition d'étape. Body : { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Validation whitelist → 400 sur étape/statut inconnu. Response : même shape que GET. Émet onboarding_step_completed/onboarding_step_skipped dans activity_log uniquement sur changed ; lors de la transition à 5/5, émet aussi onboarding_completed avec duration_ms. |
| POST | /onboarding/dismiss |
Ferme le panneau (dismissed_at = now). Idempotent. Émet onboarding_dismissed au premier appel avec payload {completed_count}. |
| POST | /onboarding/replay |
Rouvre le panneau fermé (dismissed_at = NULL). L'état des étapes n'est pas affecté. Émet onboarding_replayed lors du clear-event. |
| POST | /projects/:name/active-issue |
Issue #115. Lie la session web actuelle à une issue. Body : { issue_id: number, title?: string }. Écrit un événement session_active_issue dans activity_log (source=web). |
| GET | /projects/:name/active-issue |
Issue #115. Dernière issue liée pour ce propriétaire dans les 7 derniers jours. Response : { active_issue_id, title, ts }. |
| GET | /onboarding/cli-status |
Phase 54.3 (issue #58). L'utilisateur s'est-il connecté via arc login ces 30 derniers jours ? Response : { installed: boolean, last_cli_at: string|null }. SSOT — lignes dans activity_log avec event_type='cli_invocation' et actor=chatId. Le checklist d'onboarding frontend poll cet endpoint toutes les 10s tant que l'étape CLI est pending ; quand installed=true — marque automatiquement l'étape cli comme completed. |
| GET | /analytics/onboarding-funnel |
Phase 54.6 (issue #61). Stats de funnel agrégées sur une fenêtre glissante. Query : hours=168 (1-720, défaut 7j). 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 — événements activity_log onboarding_step_* + onboarding_completed + cli_invocation. TTFC = time-to-first-arc (delta julianday entre le premier onboarding-step et le premier cli_invocation par actor). |
Le SSOT pour les métriques de funnel (Phase 54.6 / issue #61) correspond aux événements dans activity_log (event_type LIKE 'onboarding_%'). La table onboarding_progress est un cache dérivé : l'UI se rend en une seule requête plutôt qu'en agrégeant les événements.
Beta Feedback (Phase 53.3)
| Méthode | Chemin | Description |
|---|---|---|
| POST | /feedback |
Envoyer un feedback bêta. Body : {type: "bug"|"feature"|"other", title, description, project?, browser?}. Écrit dans activity_log (event_type=feedback_report) et envoie un ping CEO sur Telegram. |
| GET | /admin/feedback |
Liste des dernières soumissions (admin uniquement). Query : limit=50 (max 500). Response : {items: [...], count}. |
POST /feedback — validation du body : type ∈ {bug,feature,other}, title ≤200 chars, description ≤5000 chars. Succès → {ok: true, type, title}. Le ping Telegram est formaté comme 🐞/💡/📝 New <type> feedback ... From: <user> Title: <title> + les 400 premiers caractères de la description.
Le widget flottant dans
FeedbackWidget.jsx(CRM dashboard) transmet automatiquementbrowser(UA + viewport + locale) etproject(technical_name du projet actif).
Beta Invites (Phase 52.1, admin uniquement)
| Méthode | Chemin | Description |
|---|---|---|
| GET | /admin/invites |
Liste de tous les codes d'invitation + compteurs (total_active, total_used). Admin uniquement. |
| POST | /admin/invites |
Générer N codes. Body : {count: N, note?: string}. Admin uniquement. Response : {ok, codes, count}. |
| DELETE | /admin/invites/:code |
Révoquer un code d'invitation non utilisé. |
Mise à jour du flux auth : POST /api/auth/register requiert désormais le champ invite_code (bêta fermée Phase 52.1). Sans code → 403 {error: "invite_required"}. Code invalide/utilisé → 403 {error: "invalid_invite"}.
Billing (Phase 51)
| Méthode | Chemin | Description |
|---|---|---|
| GET | /account/usage |
Historique d'utilisation des tokens pour l'utilisateur autorisé (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 | /billing/status |
Plan actuel, limites, usage, features. Crée automatiquement une ligne Free au premier appel. Response : { plan, status, current_period_end, limits, usage, features, pricing, can_upgrade, stripe_ready } |
| POST | /billing/checkout-session |
Stripe Checkout (Stage 2 — retourne 501 tant que le SDK Stripe n'est pas intégré) |
Limites du plan (sémantique OR) :
- Free : 1 projet ET 5 workers
- Min ($4.99/mo) : 5 projets OU 25 workers au total
- Max ($11.99/mo) : 20 projets OU 150 workers au total
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 la vérification des limites de plan — ce sont des opérateurs, pas des tenants payants.
Les bêta-testeurs (
subscriptions.plan='beta', Phase 52 F&F) contournent aussi — nombre illimité de projets/workers plus toutes les features Max. Assigné manuellement :UPDATE subscriptions SET plan='beta' WHERE user_id=?.
Bugfix (issue #25) :
POST /projects/create(Quick Start, Phase 50.2) plantait avecownerChatId is not definedà cause d'une typo — corrigé, l'audit-actor est désormais correctement enregistré.
Bugfix (issue #26) :
allocatePort()pour les nouveaux projets sonde désormais les vrais bindings TCP (ss -tln), et non plus uniquement le registry. Auparavant, il pouvait retourner un port occupé par un service hors-registry (NotebookLM bridge :19213, internal bridges) → le workspace bot plantait sur EADDRINUSE.
Flux auth (Phase 50.1) : /api/auth/register et /api/auth/login retournent désormais un JWT même pour un email non vérifié + flag needs_verification: true. Les actions sensibles (trial grant, billing, invites) vérifient email_verified séparément. Rate limit sur l'inscription : 3 / IP / 24h.
Projets (9 endpoints)
| Méthode | Chemin | Description |
|---|---|---|
| GET | /projects |
Liste des projets de l'utilisateur |
| POST | /projects/create |
Crée un projet |
| GET | /projects/:name |
Détails du projet |
| GET | /projects/:name/config |
Configuration du projet |
| PUT | /projects/:name/config |
Mettre à jour la configuration |
| 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 | /projects/:name/workers |
Liste des workers |
| POST | /projects/:name/workers |
Crée un worker |
| POST | /projects/:name/workers/reorder |
Phase 53.8 — réordonner les workers. Body : {order: [id1, id2, ...]}. Réécrit workers_registry.json de façon atomique. Les workers absents de order sont ajoutés à la fin (protection contre la perte). Response : {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 prompt système |
| 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). Response : {ok, started, bot_username}. |
| DELETE | /projects/:name/workers/:id/telegram-token |
Supprimer le token Telegram |
| POST | /projects/:name/workers/:id/notify |
Phase 53.2 — envoyer un ping d'événement TG ({event?, text, buttons?}). Silent no-op si aucun token lié ou CRM_DISABLE_TG_NOTIFY=1. |
| POST | /projects/:name/workers/:id/suggest-bot-username |
53.11.1 (issue #48) — retourne 5 candidats de username TG pour le wizard de création de bot au format <project>_<worker>_bot + fallbacks numérotés. Slugify supprime les tirets, troncature à 32 chars (la partie worker est tronquée en premier). Response : {candidates: string[]}. |
| POST | /metrics/wizard |
53.11.1 (issue #48) — sink de télémétrie pour le wizard de création de bot. Body : {action, duration_ms?, attempts?, success?, project?, worker_id?}. Écrit dans activity_log (event_type=wizard_metric), best-effort. |
| GET | /analytics/wizard-metrics?hours=168 |
53.11.1 (issue #48) — résumé du funnel : {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Défaut 7 jours, clamp 1-720h. |
| POST | /projects/:name/restart |
Redémarrer un worker |
| GET | /projects/:name/active-role |
Rôle actif actuel |
| 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/"]
}
max_turnsvaut20par défaut (auparavant5, ce qui provoquait l'erreur "Reached max turns" dans les dialogues multi-étapes avec tool calls).
POST /projects/:name/restart — query : worker_id
Fichiers et stockage (8 endpoints)
| Méthode | Chemin | Description |
|---|---|---|
| GET | /projects/:name/files |
Arborescence des fichiers |
| POST | /projects/:name/files/upload |
Envoyer un fichier (multipart, max 100 Mo) |
| POST | /projects/:name/files/mkdir |
Crée un répertoire |
| POST | /projects/:name/files/create |
Crée un fichier |
| GET | /projects/:name/files/read |
Lire un fichier |
| PUT | /projects/:name/files/save |
Enregistre 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 du projet
| Méthode | Chemin | Description |
|---|---|---|
| GET | /projects/:name/skills |
Liste des skills du projet |
| POST | /projects/:name/skills |
Crée une skill |
| PUT | /projects/:name/skills/:id |
Mettre à jour une skill |
| DELETE | /projects/:name/skills/:id |
Supprimer une skill |
Marketplace global
| Méthode | Chemin | Description |
|---|---|---|
| GET | /skills |
Liste des skills globales |
| POST | /skills |
Publier une skill |
| GET | /skills/:id |
Détails d'une skill |
| PUT | /skills/:id |
Mettre à jour une skill |
| DELETE | /skills/:id |
Supprimer une skill |
Évolution et mises à jour
| Méthode | Chemin | Description |
|---|---|---|
| GET | /skills/:id/evolution |
Historique d'évolution d'une skill |
| GET | /skill-updates |
Liste des mises à jour disponibles |
| POST | /skill-updates/:id/approve |
Accepter 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 des forks |
| POST | /projects/:name/skill-forks |
Crée un fork |
| PUT | /projects/:name/skill-forks/:id |
Mettre à jour un fork |
| DELETE | /projects/:name/skill-forks/:id |
Supprimer un fork |
Chat et messages
| Méthode | Chemin | Description |
|---|---|---|
| POST | /projects/:name/chat |
Envoyer un message dans le chat |
| GET | /projects/:name/chat/history |
Historique du chat |
| POST | /projects/:name/message |
Envoyer un message à un worker (Phase 48.6 : wake-up automatique du worker idle-killed, ~2-4s cold start ; Phase 48.6.1 : le wake-up fonctionne aussi dans les projets single-mode, pas seulement parallel) |
| GET | /projects/:name/pins |
Liste des notes (pins) |
| POST | /projects/:name/pins |
Crée 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 wiki |
| GET | /projects/:name/wiki/file |
Lire une page wiki |
| PUT | /projects/:name/wiki/save |
Enregistre une page wiki |
| GET | /projects/:name/wiki/download |
Télécharger le wiki en archive ZIP |
Analytique (4 endpoints)
| Méthode | Chemin | Description |
|---|---|---|
| GET | /analytics/activity |
Fil d'activité |
| GET | /analytics/sidebar |
Données pour le panneau latéral |
| GET | /analytics/phases |
Liste des phases du projet |
| POST | /analytics/phases |
Mettre à jour les phases du projet |
Marketplace et 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 d'une skill |
| POST | /sage/scout/install |
Installer une skill |
| POST | /sage/analyze |
Analyse Sage |
| GET | /sage/status |
Statut du service Sage |
| POST | /sage/benchmark |
Lancer un benchmark |
Mémoire et Knowledge
| Méthode | Chemin | Description |
|---|---|---|
| POST | /projects/:name/memory/refresh |
Mettre à jour la mémoire neurale |
| POST | /projects/:name/memory/fetch-artifact |
Télécharger un artefact |
| GET | /projects/:name/learnings |
Liste des learnings |
| POST | /projects/:name/learnings |
Ajouter un learning |
| GET | /projects/:name/knowledge-graph |
Graphe de connaissances du projet |
Documentation (globale, sans auth)
| Méthode | Chemin | Description |
|---|---|---|
| GET | /docs/tree?lang=<lang> |
Arborescence de la documentation ; lang optionnel (en/uk), défaut en |
| GET | /docs/file?path=<p>&lang=<lang> |
Lire un fichier de documentation avec language fallback |
GET /docs/tree — query : lang (optionnel)
- Cherche d'abord
docs/public/<lang>/index.md, fallback surdocs/public/index.md - La response inclut :
sections,files,served_lang,is_fallback,requested_lang
GET /docs/file — query : path (obligatoire), lang (optionnel)
- Ordre de résolution :
docs/public/<lang>/<path>→docs/public/<path>(fallback EN) - La response inclut :
path,content,size,modified,served_lang,is_fallback,requested_lang - 403 sur path traversal, 404 sur fichier manquant
- Phase 52.1.3 — paramètre
langajouté pour la traduction UK
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 | Non trouvé |
| 409 | Conflit (doublon) |
| 429 | Trop de requêtes |
| 500 | Erreur serveur |
GitHub Integration (Phase 49.3)
| Endpoint | Méthode | Description |
|---|---|---|
/api/crm/projects/:name/github |
GET | Liste des repos GitHub liés au projet |
/api/crm/projects/:name/github |
POST | Lier un repo (body : {owner, repo}) — retourne webhook URL + secret + instructions de setup |
/api/crm/projects/:name/github/:id |
DELETE | Délier un repo |
/api/crm/projects/:name/github/events |
GET | Liste des derniers événements GitHub (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.
Account Security (Phase 45.4)
| Endpoint | Méthode | Description |
|---|---|---|
/api/crm/account/recovery |
GET | Liste des recovery keys actives |
/api/crm/account/recovery |
POST | Crée une recovery key (body : encryptedKey, keyHint) |
/api/crm/account/recovery |
DELETE | Révoquer une ou plusieurs recovery keys (body : { id } ou {} pour toutes) |
/api/crm/account/recovery/restore |
GET | Récupérer la clé maître chiffrée pour la restauration |
Sécurité
- Multi-tenancy : chaque endpoint
:namevérifie la propriété viachatIddepuis le JWT - Validation du nom de projet :
^[a-zA-Z0-9][a-zA-Z0-9_-]*$(max 64 caractères) - Protection contre le path traversal :
safePath()sur tous les chemins contrôlés par l'utilisateur - Upload de fichier : max 100 Mo, extensions bloquées (
.exe,.bat,.sh) - CORS : origines en whitelist via
CRM_ALLOWED_ORIGINS - Protection SSRF : allowlist sur
handleScoutAnalyze— HTTPS uniquement + hôtes autorisés - Endpoints internes : rejettent les requêtes avec des en-têtes proxy (
X-Forwarded-For,X-Real-IP) - Chiffrement at-rest (Phase 45) : les clés API et messages de chat sont chiffrés AES-256-GCM
- En-têtes de sécurité :
Content-Security-Policy,X-Frame-Options: DENY,X-Content-Type-Options: nosniff - Sanitisation PII : emails, clés API, JWT sont automatiquement expurgés des logs JSONL
Phase 53.13 — baseline type-safety (2026-05-10)
Aucun changement de comportement des endpoints — uniquement des types internes. tsc --noEmit bloque désormais push/CI :
- L'interface
ChildBotest consolidée dansshared/routes/_utils.ts(3× doublons fusionnés).bot_username,heartbeat_file,health_endpoint,statussont rendus optionnels — ils reflètent le runtime-state (les entrées workspace enrichies en DB en sont souvent dépourvues). requireAdmin()dansshared/routes/system.tsretourne désormaisResponse | { userId }au lieu de{ ok, ... }— narrowing plus simple viainstanceof Response. Comportement externe inchangé (codes 401/403, corps des réponses).workers.tsDEFAULT_WORKERSa perduas const(pour la compatibilité avec les callsites mutables) ; le parsing du body pourtools/focus_dirspasse désormais strictement parArray.isArrayau lieu du fallback||.
Phase 53.15 — Sentinel Sprint 1 (2026-05-10)
Changements de comportement des endpoints auth + admin (corrections P0 de l'audit Sentinel) :
POST /api/auth/login— quandrequires2fa=true, la réponse est désormais{requires2fa: true, challenge_token}au lieu de{requires2fa: true, userId}. Le frontend doit transmettrechallenge_tokenà l'étape suivante.POST /api/auth/2fa/login— shape du body :{challenge_token, code}au lieu de{userId, code}. Token à usage unique, TTL 5 min. Sans token valide, l'endpoint retourne401 "Invalid or expired challenge — restart login". Rate-limit par userId : 5 tentatives / 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— admin uniquement. Non-admin → 403Forbidden — admin only. Sans auth → 401.- Rate-limit Nginx sur
/api/auth/*— 5 req/min/IP (burst=10 nodelay → 429). Même chose sur/api/webhooks/github(30 req/min/IP, burst=20). - HSTS — l'en-tête
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadest désormais envoyé avec chaque réponse HTTPS. Requêtes HTTP → redirect 301 vers HTTPS. X-Frame-Options: DENYau lieu deSAMEORIGIN.
Phase 53.21 — Sentinel P2 batch 2 (2026-05-12)
POST /api/crm/feedback— requiert désormais que l'appelant ait accès aubody.projectdéclaré (vérification canAccessProject). Non-propriétaire du projet → 403"Project not accessible".projectvide/absent est toujours autorisé (feedback global).POST /api/internal/trial/consume— shape du body modifié :{project, owner_id, tokens}au lieu de{project, tokens}.owner_idest obligatoire, vérifié contreprojects.owner_iden DB. 404 sur projet inconnu, 403 sur owner mismatch. L'appelant (child-bot/claude-runner.ts) propageARC_TRIAL_OWNERenv injecté parworker-spawn.ts.
Phase 53.18 — correction fuite secret tmux (2026-05-11)
Aucun changement de comportement des endpoints — uniquement un refactor des chemins de spawn internes.
POST /api/crm/onboarding/setup(viashared/routes/onboarding.ts:startWorkspaceBot) — le mode de démarrage du child-bot workspace-mode est passé debash -c "export X='val'; bun run bot.ts"àtmux -e VAR=val ... bun run bot.ts. Les valeurs des tokens ne se retrouvent plus dans/proc/PID/cmdline. Externellement : 0 changement (corps de réponse, codes de statut, comportement identiques).
Phase 53.16 — Sentinel Sprint 2 (2026-05-10)
Changements de comportement des endpoints après le hardening 13 × P1 :
- OAuth callback — L'URL de redirect utilise le fragment
#token=au lieu du query?token=(Sentinel P1-8). Le frontend lit depuiswindow.location.hash(avec fallback sur?token=pour un cycle de déploiement). /api/crm/analytics/activity+/api/crm/analytics/sidebar— la query est désormais scopée parowner_idde l'utilisateur connecté. Un non-admin ne voit que ses propres projets. Auparavant, les 80 premiers chars de chaque message assistant + les noms de projets + les IDs de workers de tous les tenants étaient exposés (Sentinel P1-4).PUT /api/crm/projects/:name/files/save— ajout d'une vérificationisProtectedPath()..env/CLAUDE.md/.git/*/.claude/*retournent désormais 403"Protected path"(auparavant, il était possible d'écraser ces fichiers) (Sentinel P1-3).POST /api/crm/projects/:name/files/mkdir+/files/create— body.name contenant..,.,/,\→ 400. Re-exécution desafePath()aprèsjoin()(Sentinel P1-2)./ws/local-bridge— le chatId du JWT est conservé lors de l'upgrade. Un message init avecproject_namen'appartenant pas à l'utilisateur → close 1008Forbidden — project not accessible. Auparavant, n'importe quel utilisateur pouvait init un bridge sur le projet d'un autre (Sentinel P1-5).- CSP — le HTML frontend (via docker/nginx.conf) envoie désormais un CSP strict :
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'. Le CSP JSON API a perdu'unsafe-inline'(Sentinel P1-10). - Helper interne
extractChatId— vérifie désormais la signature avecverifyTokenavant de décoder (Sentinel P1-6, defense-in-depth pour les futures routes skipAuth). - Format chiffré des recovery keys — les nouvelles clés sont stockées sous la forme
v2:<base64-salt>:<payload>(salt aléatoire de 16 bytes par clé). Les anciennes (sans préfixev2:) fonctionnent via un fallback legacy (Sentinel P1-13). - CEO_CHAT_ID — désormais env-first (avec fallback sur bot_registry avec avertissement). Le 474903718 hardcodé a été supprimé de 6 fichiers (Sentinel P1-14).
- Nginx X-Forwarded-For — overwrite au lieu d'append dans les 17 callsites (Sentinel P1-11). Le helper
clientIplit le DERNIER segment XFF (Sentinel P1-7).
Phase 55 — Cosmic Editorial login (2026-05-13)
Nouveaux endpoints pour la connexion via magic-link :
POST /api/auth/magic-link/request— body{ email }. Génère un token à usage unique de 10 min dansephemeral_tokens(typemagic_link), envoie le lienhttps://<host>/?magic_token=<token>via le fournisseur email. Anti-énumération : retourne toujours 200 OK avec le corps{ ok: true, message: "If the account exists, a magic link has been sent" }(même si l'email n'existe pas). Rate-limit : 3/min par (IP+email) + 5/10min par email — même contrat queforgot-password. Le chemin d'échec passe par un timing pad.POST /api/auth/magic-link/verify— body{ token }. Consomme le token à usage unique, retourne{ ok: true, token: <jwt>, userId }en cas de succès ou 401"Invalid or expired magic link". Effet de bord :user.email_verified = true+last_loginmis à jour (preuve inbox = vérification).
L'union EphemeralTokenType est étendue : elle contient désormais "magic_link" aux côtés des types existants oauth_state / password_reset / email_verification / tfa_challenge.
Le frontend (CosmicCard.jsx) gère l'état magic (countdown de renvoi de 60 s) et le paramètre URL ?magic_token= (auto-consume → login → animation de succès).
Phase 56 — AI Interop / Project Context Export (2026-05-13)
Export réservé au propriétaire d'un snapshot sanitisé du projet en .md pour transmission à un AI externe (Gemini / ChatGPT / Perplexity / Claude.ai).
GET /api/crm/projects/:name/context-export— params :include=section1,section2,...(sections :identity / workers / architecture / issues / activity / commits / learnings; défaut = les 7),scanOnly=true|false,activityHours=N(1-720, défaut 168),commitLimit=N(1-200, défaut 20),issueStatus=open|closed|all. Propriétaire uniquement — le rôle admin ne bypasse PAS (par conception). Le bypass CEO fonctionne. Retourne{ project, exportedAt, filename: "<project>-context-YYYY-MM-DD.md", scanOnly, sections, markdown, findings, stats, alertFired, preferences }. Expurge automatiquement les findings critiques sauf sipreferences.auto_redact_critical = false. Les exports non-scanOnlyécrivent dansexport_audit_log.GET /api/crm/projects/:name/exports— liste d'audit (propriétaire uniquement). Params :limit=N(1-200, défaut 50). Retourne{ project, exports: [{ id, owner_id, exported_at, sections[], findings_critical/high/medium/low, bytes }] }.GET /api/crm/projects/:name/settings/export— lire les préférences (propriétaire uniquement). Retourne{ project_name, always_include_emails, auto_redact_critical, notify_on_export, updated_at }.PATCH /api/crm/projects/:name/settings/export— mettre à jour les préférences (propriétaire uniquement). Le body accepte n'importe quel sous-ensemble de{ always_include_emails, auto_redact_critical, notify_on_export }(booléens). Retourne les préférences mises à jour.GET /api/crm/analytics/exports— stats agrégées (auth requise, pas de gate propriétaire — carte analytique). Param :hours=N(1-720, défaut 168). Retourne{ total, byProject: [{ project_name, n, last }], severitySums: { critical, high, medium, low } }.
Alerte : quand le propriétaire dépasse 3 exports en 24h ET prefs.notify_on_export = true (défaut OFF) — logActivity("export_alert", ...) transite par le pipeline de notification TG Phase 53.10 existant (alertFired: true dans le corps de la réponse).
Scanner multi-niveaux (shared/secret-scanner.ts) — Tier 1 regex (PATTERN_REGISTRY depuis le sanitiseur PII), Tier 2 entropie Shannon ≥4,5 bits/char sur des séquences ≥20 chars, Tier 3 heuristiques contextuelles (key=/token:/secret=/password=). Whitelist : UUID / git SHA / SHA-256 / caractères répétés / hex court / base58 basse entropie. Niveaux 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 CRM au lieu de ssh/edit-.env/paste-in-chat. MVP backend (Stage 1 sur 4 stages). Tous les endpoints sont gateés par requireAdmin (Phase 53.15) — retournent 403 Forbidden — admin only pour les non-admin, 401 Unauthorized sans JWT.
GET /api/crm/platform/settings— retourne{ items: [{ name, label, description, testable, restartTargets[], set, preview, length, lastRotated, lastRotatedBy }] }. Allowlist de 9 clés (ANTHROPIC_API_KEY,PLATFORM_ANTHROPIC_KEY,GITHUB_CLIENT_ID/SECRET,GOOGLE_CLIENT_ID/SECRET,MASTER_BOT_TOKEN,CITADEL_BOT_TOKEN,RESEND_API_KEY). Aperçu expurgé :prefix(12)…suffix(4)+ longueur. La valeur complète ne quitte jamais le serveur.PUT /api/crm/platform/settings/:name— body{ value: string ≥ 8 chars }. Écrit atomiquement dans le vault viastoreSecret(name, value)+ ligne d'audit. 400 si name n'est pas dans l'allowlist ; 400 si value < 8 chars ; 500 en cas d'échec d'écriture vault.POST /api/crm/platform/settings/:name/test— vérifie auprès de l'API SaaS. Anthropic →GET /v1/modelsavecx-api-key; TG →getMe; Resend →/api-keys. Les secrets client OAuth standalone ne sont pas testables → 501. Retourne{ ok: bool, reason?: string, detail?: string }. Timeout 8 secondes viaAbortController.POST /api/crm/platform/settings/:name/restart—Bun.spawn(["nohup", "bash", "-c", "sleep 1 && tmux kill-session ... && bash start-*.sh"], { detach: true })sur les sessions tmux liées. Détaché pour que le restart du master ne tue pas la réponse en cours. Retourne{ ok: true, restarted: [sessions], note }.GET /api/crm/platform/audit?limit=50&key=ANTHROPIC_API_KEY— entrées récentes du log d'audit, du plus récent au plus ancien (limit plafonné à 500). Filtre optionnel par clé.
Liste d'exclusion stricte NEVER_EXPOSE : CRM_SECRET (signature JWT) + SECRET_ENCRYPTION_KEY (méta-clé vault) — même une requête admin avec un token valide retourne 400 "not managed". Le log d'audit est en append-only (pas de handler UPDATE/DELETE), chaque action (y compris les échecs) écrit une ligne avec IP + UA + email.
Migration DB 026 — table platform_audit_log. Stage 2 (frontend PlatformSettings.jsx) — livré 2026-05-15 (cbc8bac) : grille de cartes admin uniquement + modal de rotation (<input type="password"> + confirmation de saisie) + drawer d'audit ; entrée dans la sidebar filtrée par userRole === "admin" récupéré depuis /api/auth/me.
Polish (2026-05-15, commit 56191b0) — Restructuration 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/flux qui consomment la clé), getFromUrl (où obtenir une nouvelle valeur), effectAfterRotate, riskIfLeaked. Utilisés par le frontend pour afficher 4 groupes de cartes par section + panneau d'aide repliable par carte avec contexte structuré (Used in / Get from / Effect / Risk). Aucun changement de comportement des endpoints mutateurs (PUT/POST/restart/test).
Refactor (2026-05-16) — nettoyage interne de shared/routes/platform.ts. 39 lignes supprimées (16 ajoutées), aucun changement de surface API publique. Les signatures et réponses des endpoints PUT/POST/restart/test/audit sont inchangées. Documenté ici uniquement parce que le hook pre-push de couverture doc 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 ts optionnel (chaîne ISO-8601). Utilisé par la reconstruction arc retro pour que les entrées historiques atterrissent à leur timestamp d'origine. Les valeurs à date future sont silencieusement plafonnées à now dans addActivity() (défense contre les erreurs de saisie). ISO invalide → 400.
Stage 3 (2026-05-15) — hot-reload des secrets OAuth + Resend sans restart. shared/auth.ts loadOAuthConfig() lit désormais getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") à chaque 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 désormais effet dès la prochaine requête, sans restart requis. restartTargets pour ces 5 clés est vide → le bouton Restart est masqué dans l'UI. Cas limite : un flux OAuth avec un state-token émis avant la rotation peut recevoir un 400 au callback lors du code-exchange — un retry utilisateur résout le problème. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN restent restart-required (lus lors du spawn du child-bot / initialisation du long-poll TG).
Nettoyage Phase 57.3.5 (2026-05-16) — l'allowlist MANAGED_KEYS réduite de 9 à 6. Supprimés : ANTHROPIC_API_KEY (les opérateurs utilisent désormais PLATFORM_ANTHROPIC_KEY pour les trial-credits et l'inférence plateforme ; le fallback .env fonctionne toujours pour les chemins de code legacy jusqu'à la migration de Sage/Karpathy), CITADEL_BOT_TOKEN (le bot par projet appartient aux entrées vault child:<name>:token, géré par le flux d'onboarding worker — pas les Platform Settings). MASTER_BOT_TOKEN remis à jour : label → "Telegram — System Monitor Bot", description → "Server health alerts + on-demand status probes (admin-only, not a chat bot)". La Phase 58 ajoutera la boucle de monitoring (push alerts pour crash worker / disque / RAM / brute-force SSH / bypass CF + commandes /status, /health, /errors, /restart). Set final : PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (refs #103).
Phase 63 — Consolidation UI/UX + Suivi de l'utilisation des tokens (2026-05-21, #148)
Nouvel endpoint :
POST /api/internal/usage/log(loopback uniquement) — écrit une ligne danstoken_usage_log. Body :{ project_name, owner_id, worker_id?, input_tokens, output_tokens, cache_tokens, total_tokens }. Appelé depuischild-bot/bot.tsen fire-and-forget après chaque appel Claude (callClaudeOnce+callWorkertext path). Aucun header auth requis —/api/internal/*n'est accessible que depuis localhost et bloqué par nginx pour les requêtes externes.GET /api/crm/account/usage— historique d'utilisation des tokens pour l'utilisateur autorisé (décrit dans le tableau Onboarding ci-dessus).
Modifications dans claude-runner.ts :
callClaudeOnce+callWorkertext path : maintenant toujours--output-format json(avanttextpour non-trial). Le parse JSON extraitresultcomme texte de sortie etusagepour le logging. Le flux trial consume est inchangé.- Nouveau dep
logUsage?dansClaudeRunnerDeps— callback(workerId, { input, output, cache }) => void.
Modifications UI (pas API) :
UserDropdown: composantUsageCardavec total tokens + « Details → » à l'ouverture ; point d'avertissement sur l'avatar quand le solde trial < 20 %.BillingPage: section Token Usage avec barre de totaux + tableau de 50 lignes. Plan Enterprise (en développement). Toggledetailssur chaque carte.OnboardingProgressPill: redessiné en dropdown inline dans le header (plus de wizard modal).WorkerSelector: variables CSS sémantiques--worker-{role}à la place des tokens Tailwind chart.