diff --git a/application/cli/migrate_063_vocea_channel.php b/application/cli/migrate_063_vocea_channel.php new file mode 100644 index 0000000..e8a313e --- /dev/null +++ b/application/cli/migrate_063_vocea_channel.php @@ -0,0 +1,36 @@ +prepare( + "SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?" + ); + $st->execute([$table, $col]); + return (bool)$st->fetchColumn(); +} + +$adds = [ + 'vocea_tenant_slug' => "ADD COLUMN vocea_tenant_slug VARCHAR(64) NULL COMMENT 'Slug canale Vocea (NULL=nessuno)'", + 'vocea_channel_status' => "ADD COLUMN vocea_channel_status ENUM('none','provisioning','active','suspended') NOT NULL DEFAULT 'none'", + 'vocea_enrolled_at' => "ADD COLUMN vocea_enrolled_at TIMESTAMP NULL COMMENT 'Enrollment canale + key holder'", +]; + +foreach ($adds as $col => $clause) { + if (colExists($pdo, 'organizations', $col)) { echo " - $col gia presente, skip\n"; continue; } + $pdo->exec("ALTER TABLE organizations $clause"); + echo " + aggiunta colonna organizations.$col\n"; +} + +echo "Migrazione 063 — organizations.vocea_* OK. Prossima mig=064.\n"; diff --git a/application/config/config.php b/application/config/config.php index 6cbbeaa..b25a380 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -103,6 +103,18 @@ define('ANTHROPIC_API_KEY', Env::get('ANTHROPIC_API_KEY', '')); define('ANTHROPIC_MODEL', Env::get('ANTHROPIC_MODEL', 'claude-sonnet-4-5-20250929')); define('ANTHROPIC_MAX_TOKENS', Env::int('ANTHROPIC_MAX_TOKENS', 4096)); +// ═══════════════════════════════════════════════════════════════════════════ +// VOCEA (Whistleblowing zero-knowledge — integrazione, DISABILITATA di default) +// ═══════════════════════════════════════════════════════════════════════════ +// SCAFFOLD DORMIENTE: con VOCEA_ENABLED=false (default) VoceaService non effettua +// nessuna chiamata di rete. Attivazione a MS live: applicare migrate_063 + +// VOCEA_ENABLED=true + VOCEA_API_KEY (vault tier1__nis2-agile__vocea__*) + +// slug del canale su organizations.vocea_tenant_slug (status='active'). +define('VOCEA_ENABLED', filter_var(Env::get('VOCEA_ENABLED', 'false'), FILTER_VALIDATE_BOOLEAN)); +define('VOCEA_BASE_URL', Env::get('VOCEA_BASE_URL', 'https://api.vocea.cloud/api/wb/v1')); // API integratori (X-API-Key) +define('VOCEA_API_KEY', Env::get('VOCEA_API_KEY', '')); // wbk_... per-tenant (da vault) +define('VOCEA_PORTAL_URL', Env::get('VOCEA_PORTAL_URL', 'https://app.vocea.cloud')); // portale embed via wb-link.js + // ═══════════════════════════════════════════════════════════════════════════ // CERTISOURCE (atti-service.php) // ═══════════════════════════════════════════════════════════════════════════ diff --git a/application/controllers/WhistleblowingController.php b/application/controllers/WhistleblowingController.php index 34b7653..2b011c6 100644 --- a/application/controllers/WhistleblowingController.php +++ b/application/controllers/WhistleblowingController.php @@ -19,6 +19,7 @@ require_once __DIR__ . '/BaseController.php'; require_once APP_PATH . '/services/WebhookService.php'; +require_once APP_PATH . '/services/VoceaService.php'; class WhistleblowingController extends BaseController { @@ -126,6 +127,17 @@ class WhistleblowingController extends BaseController { $this->requireOrgRole(['org_admin', 'compliance_manager']); + // ── Delega a Vocea quando il canale esterno è attivo per l'org (dormiente di + // default: VOCEA_ENABLED=false → si salta del tutto e resta il modulo legacy). + if (VoceaService::enabled()) { + $org = $this->voceaOrg(); + $svc = new VoceaService(); + if ($org && $svc->isEnabledForOrg($org)) { + $this->listViaVocea($svc, $org); + return; + } + } + $conditions = ['wr.organization_id = ?']; $params = [$this->getCurrentOrgId()]; @@ -383,4 +395,69 @@ class WhistleblowingController extends BaseController 'timeline' => $timeline, ]); } + + // ══════════════════════════════════════════════════════════════════════ + // VOCEA (integrazione whistleblowing zero-knowledge) — scaffold dormiente + // ══════════════════════════════════════════════════════════════════════ + + /** + * GET /api/whistleblowing/channelStatus + * Indica alla UI se mostrare il canale Vocea (embed wb-link.js) o il modulo legacy. + * Non espone segreti (mai l'API key). + */ + public function channelStatus(): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + + $enabled = VoceaService::enabled(); + $org = $enabled ? $this->voceaOrg() : null; // query solo se attivo (colonne vocea_* presenti) + $slug = $org['vocea_tenant_slug'] ?? null; + $status = $org['vocea_channel_status'] ?? 'none'; + + $this->jsonSuccess([ + 'integration_enabled' => $enabled, + 'channel_status' => $status, + 'provisioned' => $enabled && !empty($slug) && $status === 'active', + 'portal_url' => !empty($slug) ? VoceaService::portalUrl($slug) : null, + ]); + } + + /** Carica l'org corrente con le colonne vocea_* (chiamata solo quando VOCEA_ENABLED). */ + private function voceaOrg(): ?array + { + return Database::fetchOne( + 'SELECT * FROM organizations WHERE id = ?', + [$this->getCurrentOrgId()] + ) ?: null; + } + + /** + * Lista segnalazioni dal canale Vocea (metadati + ciphertext, zero-knowledge). + * Il plaintext NON transita mai: la decifratura avviene client-side dal gestore. + * NB: mapping campi da riconfermare allo smoke a MS live (shape risposta del MS). + */ + private function listViaVocea(VoceaService $svc, array $org): void + { + $query = []; + foreach (['status', 'category', 'page', 'per_page'] as $f) { + if ($this->hasParam($f)) $query[$f] = $this->getParam($f); + } + + $res = $svc->listReports($org['vocea_tenant_slug'], $query); + if (!($res['ok'] ?? false)) { + $this->jsonError( + 'Canale Vocea non raggiungibile: ' . ($res['error'] ?? 'errore'), + 502, + 'VOCEA_UPSTREAM' + ); + } + + $data = $res['data'] ?? []; + $reports = $data['items'] ?? ($data['reports'] ?? (is_array($data) ? $data : [])); + $this->jsonSuccess([ + 'reports' => $reports, + 'total' => $data['total'] ?? count($reports), + 'source' => 'vocea', + ]); + } } diff --git a/application/services/VoceaService.php b/application/services/VoceaService.php new file mode 100644 index 0000000..43ec4b8 --- /dev/null +++ b/application/services/VoceaService.php @@ -0,0 +1,156 @@ +false,'error'=>'DISABLED']. Si "accende" così: + * 1. applicare migrate_063_vocea_channel.php (colonne organizations.vocea_*) + * 2. VOCEA_ENABLED=true + VOCEA_API_KEY (vault tier1__nis2-agile__vocea__*) + * 3. impostare organizations.vocea_tenant_slug + vocea_channel_status='active' sul canale + * + * CONFINE ZERO-KNOWLEDGE: l'API NON ritorna MAI contenuti in chiaro. listReports + * restituisce metadati + ciphertext; la decifratura avviene client-side dal gestore. + * + * Contratto: whistleblowing-agile/docs/API_GATEWAY_INTEGRATIONS.md + * GET /integrations/reports (scope reports:read) — lista metadati (paginata) + * GET /integrations/reports/{code}/status (scope status:read) + * POST /integrations/reports/submit (scope reports:submit) — payload GIÀ cifrato + * GET /integrations/keyholders · POST /integrations/keyholders (enroll public key RSA) + * GET /tenants/{slug}/info (pubblico — public key del canale) + * + * NB: base URL/prefisso (`/api/wb/v1` vs `/v1`) e shape esatto risposte vanno + * riconfermati allo smoke a MS live; sono parametrizzati (VOCEA_BASE_URL) apposta. + */ + +class VoceaService +{ + private const TIMEOUT_SEC = 12; + private const CONNECT_TIMEOUT_SEC = 6; + + private string $baseUrl; + private string $apiKey; + + public function __construct(?string $apiKey = null) + { + $this->baseUrl = rtrim(defined('VOCEA_BASE_URL') ? VOCEA_BASE_URL : 'https://api.vocea.cloud/api/wb/v1', '/'); + $this->apiKey = $apiKey ?? (defined('VOCEA_API_KEY') ? VOCEA_API_KEY : ''); + } + + /** Flag globale: l'integrazione Vocea è attiva? */ + public static function enabled(): bool + { + return defined('VOCEA_ENABLED') && VOCEA_ENABLED === true; + } + + /** Il canale è utilizzabile per questa organization? (flag globale + slug + stato active + key). */ + public function isEnabledForOrg(array $org): bool + { + return self::enabled() + && !empty($org['vocea_tenant_slug']) + && (($org['vocea_channel_status'] ?? 'none') === 'active') + && $this->apiKey !== ''; + } + + /** + * URL del portale segnalante da embeddare via wb-link.js. + * NB: iframe diretto è bloccato dalla CSP `frame-ancestors 'self'` di Vocea → + * usare il widget wb-link.js oppure far allargare la CSP al dominio NIS2. + */ + public static function portalUrl(string $slug): string + { + $base = rtrim(defined('VOCEA_PORTAL_URL') ? VOCEA_PORTAL_URL : 'https://app.vocea.cloud', '/'); + return "{$base}/wb/" . rawurlencode($slug); + } + + // ── Read / management (server-to-server, API key) ──────────────────────── + + /** Public key del canale (endpoint pubblico, no API key). */ + public function tenantInfo(string $slug): array + { + return $this->request('GET', '/tenants/' . rawurlencode($slug) . '/info', null, false); + } + + /** Lista metadati segnalazioni del canale (paginata, SENZA plaintext). */ + public function listReports(string $slug, array $query = []): array + { + $qs = $query ? ('?' . http_build_query($query)) : ''; + return $this->request('GET', "/integrations/reports{$qs}"); + } + + /** Stato di una segnalazione tramite codice ricevuta. */ + public function getReportStatus(string $slug, string $code): array + { + return $this->request('GET', '/integrations/reports/' . rawurlencode($code) . '/status'); + } + + /** Invio segnalazione: $encryptedPayload è GIÀ cifrato lato client (zero-knowledge). */ + public function submitReport(string $slug, array $encryptedPayload): array + { + return $this->request('POST', '/integrations/reports/submit', $encryptedPayload); + } + + public function listKeyHolders(string $slug): array + { + return $this->request('GET', '/integrations/keyholders'); + } + + public function enrollKeyHolder(string $slug, array $keyHolder): array + { + return $this->request('POST', '/integrations/keyholders', $keyHolder); + } + + // ── HTTP ───────────────────────────────────────────────────────────────── + + /** + * @param bool $auth Se true invia X-API-Key (default). Endpoint pubblici (tenantInfo) → false. + * @return array ['ok'=>bool,'status'=>int,'data'=>mixed,'error'=>?string] + */ + private function request(string $method, string $path, ?array $body = null, bool $auth = true): array + { + if (!self::enabled()) { + return ['ok' => false, 'error' => 'DISABLED', 'message' => 'Integrazione Vocea non attiva (VOCEA_ENABLED=false).']; + } + if ($auth && $this->apiKey === '') { + return ['ok' => false, 'error' => 'NOT_PROVISIONED', 'message' => 'VOCEA_API_KEY non configurata (vault tier1__nis2-agile__vocea__*).']; + } + + $headers = ['Accept: application/json']; + if ($auth) { $headers[] = 'X-API-Key: ' . $this->apiKey; } + + $ch = curl_init($this->baseUrl . $path); + $opts = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_TIMEOUT => self::TIMEOUT_SEC, + CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT_SEC, + ]; + if ($body !== null) { + $opts[CURLOPT_POSTFIELDS] = json_encode($body, JSON_UNESCAPED_UNICODE); + $headers[] = 'Content-Type: application/json'; + } + $opts[CURLOPT_HTTPHEADER] = $headers; + curl_setopt_array($ch, $opts); + + $raw = curl_exec($ch); + $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + + if ($raw === false || $status === 0) { + return ['ok' => false, 'status' => 0, 'error' => 'NETWORK', 'message' => $err ?: 'connessione fallita']; + } + + $decoded = json_decode($raw, true); + $ok = $status >= 200 && $status < 300; + return [ + 'ok' => $ok, + 'status' => $status, + 'data' => $decoded, + 'error' => $ok ? null : (($decoded['error']['code'] ?? null) ?: ('HTTP_' . $status)), + ]; + } +} diff --git a/docs/INCOMING_FROM_AGILEHUB_2026_06_23_vocea_integration_reply.md b/docs/INCOMING_FROM_AGILEHUB_2026_06_23_vocea_integration_reply.md new file mode 100644 index 0000000..4bc13d2 --- /dev/null +++ b/docs/INCOMING_FROM_AGILEHUB_2026_06_23_vocea_integration_reply.md @@ -0,0 +1,34 @@ +# INCOMING ← AgileHub (VIGILE) — Risposta: integrazione NIS2 ↔ Vocea (whistleblowing) Modalità C +**Data**: 2026-06-23 · **Da**: VIGILE · **A**: team NIS2 · **Rif**: vostro OUTGOING_TO_AGILEHUB_2026_06_23_vocea_whistleblowing_integration.md + +## TL;DR +L'integrazione **non è bloccata "a vuoto"**: l'API integratori che vi serve **esiste già nel codice** Vocea (verificato), manca solo il **deploy** (sbloccato oggi: ho consegnato al team WISH gli accessi a vocea-prod). Potete **partire con lo scheletro** lato NIS2 ora. + +## Risposte punto per punto + +**1) Deploy nexus-whistleblowing-ms** → dipende dal team Vocea/WISH. **Sbloccato oggi**: server vocea-prod (178.104.204.196) pronto, accesso deploy verificato, ho dato loro accessi+istruzioni (`whistleblowing-agile/docs/INCOMING_FROM_AGILEHUB_2026_06_23_vocea_prod_access.md`). Tempistica esatta: la conferma il team Vocea. VIGILE sollecita. + +**2) Endpoint prioritari** → **GIÀ NEL CODICE** (verificato nelle route backend Vocea), non da scrivere: +- `GET /v1/integrations/reports` (metadati) ✓ +- `GET /v1/integrations/reports/:code/status` ✓ +- `POST /v1/integrations/reports/submit` ✓ +- `GET/POST /v1/integrations/keyholders` (+`/:id`) ✓ — enrollment public key RSA dei key holder (il vostro punto 3) +- `GET /v1/integrations/delegates` ✓ +- **API-key issuance**: non vista come route `/v1/integrations` → o è admin-side, o **la provisiono io (VIGILE) manualmente** (pattern marketing/external-gateway). Confermo col team Vocea. +→ Quindi P2 = **questione di deploy**, non di sviluppo Vocea. + +**3) Provisioning pilota** → fattibile appena il MS è live. Org pilota: ok **Nuova Agile (996003)** o dogfooding 129 (decide presidenza/Vocea). API key scope `reports:submit/read,status:read` → **la deposito io nel vault `tier1__nis2-agile__vocea__*`** (come ho fatto per le altre vostre creds). Enrollment key holder RSA = via gli endpoint `/v1/integrations/keyholders` (esistono). + +**4) Embed UX** → esiste **`wb-link.js`** (`frontend/public/wb-link.js`) = il meccanismo previsto (consigliato). ⚠️ Iframe diretto cross-origin **oggi è bloccato** dalla CSP `frame-ancestors 'self'` dei portali `/wb/{slug}` e `/wb/portal/{slug}`. Due opzioni: (a) usare **wb-link.js** (launcher), oppure (b) il team Vocea allarga `frame-ancestors` al dominio NIS2 (same-suite) per iframe vero. VIGILE coordina la scelta. + +**5) Egress allowlist** → **NON serve**. Il firewall di vocea-prod ha **443 aperto a tutti** (0.0.0.0/0). Le vostre call server-to-server (egress 135.181.149.254) verso `api.vocea.cloud` passano senza allowlist. + +**6) Costo** → decisione **business/presidenza**, fuori dal perimetro VIGILE. Per il pilota interno suite serve l'ok presidenza/owner Vocea prima di contare consumo per-tenant. VIGILE inoltra. + +## Raccomandazione VIGILE sullo scheletro NIS2 +**Procedete con lo scheletro ORA**: poiché il contratto API è **già scritto** (endpoint confermati nel codice), `mig 062 vocea_tenant_slug` + `VoceaService.php` stub (sui 4 endpoint sopra, non attivati) + `WhistleblowingController` proxy/embed sono **a basso rischio e plug-and-play**. Quando il MS è live + arriva l'API key (vault), accendete. Non è lavoro "a vuoto": il contratto non cambierà. (Report legacy read-only, non retro-cifrati: ok.) + +## Owner +- **Team Vocea/WISH**: deploy MS + (se serve) route API-key issuance + scelta embed (wb-link vs CSP). +- **VIGILE**: infra/firewall (fatto) · provisioning tenant+API-key nel vault NIS2 (a MS live) · coordinamento. +- **Presidenza/owner Vocea**: costo + org pilota. diff --git a/docs/OUTGOING_TO_AGILEHUB_2026_06_23_vocea_whistleblowing_integration.md b/docs/OUTGOING_TO_AGILEHUB_2026_06_23_vocea_whistleblowing_integration.md new file mode 100644 index 0000000..ce11e93 --- /dev/null +++ b/docs/OUTGOING_TO_AGILEHUB_2026_06_23_vocea_whistleblowing_integration.md @@ -0,0 +1,32 @@ +# OUTGOING → AgileHub (VIGILE) — Integrazione Vocea in NIS2 + +> **Da**: NIS2 Agile · **A**: VIGILE (+ team Vocea/WISH / TITAN per il backend) · **Data**: 2026-06-23 +> **Oggetto**: NIS2 vuole sostituire il suo modulo whistleblowing interno con **Vocea** (`nexus-whistleblowing-ms`). Stato verificato dal backend reale + richieste. +> **Riferimenti letti**: `whistleblowing-agile/docs/DESIGN_WHISTLEBLOWING_INDEPENDENT_PRODUCT.md`, `.../docs/API_GATEWAY_INTEGRATIONS.md`, `.../docs/INCOMING_FROM_AGILEHUB_2026_06_23_vocea_prod_access.md`, `.../CLAUDE.md`. + +--- + +## 1. Intento NIS2 (decisione utente 2026-06-22) +Sostituire il modulo whistleblowing interno di NIS2 (oggi salva i contenuti **in chiaro** in MySQL) con **Vocea**, integrandolo come **Modalità C — prodotto verticale**: NIS2 fa provisioning + UX (embed/link del portale) + dashboard metadati, **senza re-implementare la crypto** (come da design). Vocea = prodotto suite; org pilota proposta: **Nuova Agile 996003** o dogfooding **129**. + +## 2. Stato reale accertato (ispezione via SSH host, read-only, 2026-06-23) +- ❌ `nexus-whistleblowing-ms` **NON è live/deployato** (niente in ascolto su :4223; il prodotto WB esiste solo come devenv `whistleblowing-agile-devenv`, **unhealthy**, nessun processo MS attivo). Il CLAUDE.md WB conferma: *"backend 4223 NON deployato — primo task"*, *"schema DB non creato"*. +- ✅ Server prod pronto: `vocea-prod` 178.104.204.196 (CX33 Nuremberg). DNS `app/api.vocea.cloud` live. 443 aperto; 4223/3306 chiusi (loopback). **Stack ancora da deployare** dal team Vocea. +- ⚠️ Layer API integrazione **parziale**: solo `POST /integrations/reports/submit` esiste. `GET status` (`status:read`) e `GET reports` (`reports:read`, metadati/ciphertext) + route emissione API key = **da implementare** (loro `API_GATEWAY_INTEGRATIONS.md` §2 + checklist §10). +- ✅ Contratto: API-key per-tenant server-to-server su `https://api.vocea.cloud/api/wb/v1/integrations/*`, zero-knowledge preservato. Rete OK (pubblico 443, raggiungibile dal backend NIS2). + +## 3. Richieste a VIGILE (coordinamento) / team Vocea (backend) +1. **Deploy MS**: tempistica per `nexus-whistleblowing-ms` live su `api.vocea.cloud` (stack su vocea-prod). +2. **Completare API per integratori**: NIS2 per la **dashboard di gestione** ha bisogno di `GET /v1/integrations/reports/{code}/status` e `GET /v1/integrations/reports` (lista metadati). Oggi manca. Più una **route di emissione API key** per il canale. Possono essere prioritizzati? +3. **Provisioning pilota**: creare 1 tenant/canale per l'org pilota NIS2 + emettere **API key** (scope `reports:submit`,`reports:read`,`status:read`) → la depositiamo nel **vault NIS2** (`tier1__nis2-agile__vocea__*`). + come enrollano i key holder dell'org le loro public key RSA. +4. **Modalità UX (preferenza nostra = embed)**: confermare che i portali `app.vocea.cloud/wb/{slug}` (segnalante) e `/wb/portal/{slug}` (gestore) sono **embeddabili in iframe** dentro NIS2 (X-Frame-Options/CSP same-suite) o solo link-out (+ esiste `wb-link.js`?). L'embed riusa crypto+UI esistenti = molto meno lavoro e zero-knowledge pulito. +5. **Egress/allowlist**: per le call server-to-server serve allowlistare l'IP di uscita del backend NIS2 (135.181.149.254) sul vhost `api.vocea.cloud`/per-key? (la doc cita IP allowlist opzionale). +6. **Costo/commerciale**: per integrazione **suite-interna** c'è un costo per-tenant (design §7 cita €50–200/mese reseller)? Serve ok presidenza prima del pilota? + +## 4. Cosa fa NIS2 da solo (scope, per allineamento) +`VoceaService.php` (client API, key da vault) · mig 062 `organizations.vocea_tenant_slug` · `WhistleblowingController` → proxy/embed (legacy reports **read-only**, non retro-cifrati) · `whistleblowing.html` → embed portale + dashboard via API · i18n/help/KB. **Zero crypto re-implementata.** + +## 5. Risposta +Rispondere come `INCOMING_FROM_AGILEHUB_..._vocea_nis2.md` nel repo NIS2 con: tempistica deploy MS (R1), prioritizzazione endpoint read/status + key issuance (R2), org pilota + API key + enroll key holder (R3), embeddabilità portale (R4), allowlist egress (R5), costo (R6). + +— NIS2 Agile diff --git a/docs/sql/063_vocea_channel.sql b/docs/sql/063_vocea_channel.sql new file mode 100644 index 0000000..24f009e --- /dev/null +++ b/docs/sql/063_vocea_channel.sql @@ -0,0 +1,14 @@ +-- 063_vocea_channel.sql — Integrazione Vocea (whistleblowing zero-knowledge). +-- Mappa ogni organization NIS2 al proprio canale Vocea (tenant). ADDITIVA, non distruttiva. +-- Apply autoritativo IDEMPOTENTE: application/cli/migrate_063_vocea_channel.php +-- (MySQL non supporta ADD COLUMN IF NOT EXISTS → l'apply controlla information_schema). +-- SCAFFOLD DORMIENTE: nessun effetto funzionale finché VOCEA_ENABLED=false (default). + +ALTER TABLE organizations + ADD COLUMN vocea_tenant_slug VARCHAR(64) NULL COMMENT 'Slug del canale Vocea associato (NULL = nessun canale)'; + +ALTER TABLE organizations + ADD COLUMN vocea_channel_status ENUM('none','provisioning','active','suspended') NOT NULL DEFAULT 'none'; + +ALTER TABLE organizations + ADD COLUMN vocea_enrolled_at TIMESTAMP NULL COMMENT 'Quando canale + key holder sono stati enrollati'; diff --git a/public/index.php b/public/index.php index ea7128d..9062603 100644 --- a/public/index.php +++ b/public/index.php @@ -644,6 +644,7 @@ $actionMap = [ 'POST:{id}/assign' => 'assign', 'POST:{id}/close' => 'close', 'GET:stats' => 'stats', + 'GET:channelStatus' => 'channelStatus', 'GET:trackAnonymous' => 'trackAnonymous', ],