← Developers

03.2 — Eventi inbound: POST /actions/:trigger

Spec riferita a SPEC-DX-01 / api v1

L’Invariante

Esiste un solo path di scrittura ed è questo. POST /api/v1/actions/:trigger sul nodo edge è l’unico modo in cui un fatto di business entra nel sistema — dalla dashboard operatore, da un’integrazione macchina o da qualsiasi client futuro. Se stai scrivendo qualcosa che bypassa questo endpoint, stai costruendo il bug che questa architettura esiste per prevenire.

Il Contratto

POST /api/v1/actions/:trigger
Authorization: Bearer <sessione operatore>
X-Idempotency-Key: <uuid per gesto logico>     # stessa chiave = stesso gesto, retry sicuro
Content-Type: application/json

{ "aggregateId": "<uuid opzionale>",
  "payload": { /* solo campi dichiarati dal trigger — il contesto viene dalla sessione */ } }
type ActionResult =
  | { status: 200, body: object }                        // applicata (row, to_state, …)
  | { status: 400, code: 'ACTION_FAILED' }               // constraint/validazione — fail-closed
  | { status: 401, code: 'AUTH_REQUIRED' }               // sessione mancante/scaduta
  | { status: 403, code: 'ACTION_FAILED' }               // diniego RBAC
  | { status: 409, code: 'ACTION_FAILED' }               // conflitto transizione FSM
  | { status: 409, code: 'IDEMPOTENCY_IN_PROGRESS' }     // stessa chiave in elaborazione — riprova
  | { status: 429, code: 'RATE_LIMITED' }                // rispetta Retry-After
  | { status: 503, code: 'NO_ENGINE' };                  // nessun manifest caricato sul nodo

Regole del payload: payload è l’input del trigger — tenant_id, operator_id, device_code vengono dalla sessione ($context), mai dal body; le espressioni lo leggono come $payload.*. Non esiste un campo version di optimistic-lock: l’ordinamento è garantito dalla FSM — un trigger fuori stato viene rifiutato con 409.

Il Flusso

client → trigger + payload + idempotency-key
  → edge: sessione → RBAC → constraint → FSM → data-link → txn(write+audit+outbox)
  → 200: fatto committato, entry outbox già durevole
retry su caduta rete: stessa X-Idempotency-Key → stesso esito, nessun fatto duplicato

Esempio di Payload

# QC_PASS — chiude il collaudo; il constraint engine verifica la misura
curl -X POST http://edge:8291/api/v1/actions/QC_PASS \
  -H "Authorization: Bearer $TOKEN" -H "X-Idempotency-Key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{"payload":{"piece_id":"PZ-001","misura":0.018}}'
# misura 0.026 → la regola `tolleranza_collaudo` rifiuta: fail-closed, niente commit.
// Node — il gesto intero, retry-safe
const r = await fetch(`${edge}/api/v1/actions/QC_PASS`, {
  method: 'POST',
  headers: { authorization: `Bearer ${token}`,
             'x-idempotency-key': gestureKey,   // RIUSA la chiave sul retry
             'content-type': 'application/json' },
  body: JSON.stringify({ payload }) });
if (r.status === 409) { /* conflitto FSM — l'aggregato è in un altro stato */ }
if (r.status === 400 || r.status === 403) { /* leggi j.error — constraint o RBAC */ }

I Fallimenti Noti

Codice Significato → azione correttiva
409 ACTION_FAILED Transizione FSM non ammessa dallo stato corrente → ispeziona le transitions del manifest
403 ACTION_FAILED Il ruolo non ha il trigger → rbac_engine.permissions del manifest
400 ACTION_FAILED Una pre_commit_rule o validazione è fallita → leggi error, risolvi il blocco prima
409 IDEMPOTENCY_IN_PROGRESS Stessa chiave ancora in volo → attendi e riprova con la stessa chiave
Fatti duplicati dopo retry Hai cambiato la X-Idempotency-Key al retry — stesso gesto, stessa chiave
401 AUTH_REQUIRED a metà sessione Rifai login; non ritentare lo stesso token morto in loop