
Client TypeScript snello + client Python zero-dep e ricette per proteggere azioni ad alto rischio dietro un'approvazione passkey legata al payload.
Client TypeScript leggero, zero dipendenze per Cosignet — approvazione human-in-the-loop per azioni ad alto rischio di agenti AI, con firme passkey vincolate al payload.
Metti un umano nel loop prima che un'azione pericolosa venga eseguita: mettila in pausa, ottieni
un'esplicita approvazione passkey da una persona (Face ID / Touch ID / Windows Hello / chiave
di sicurezza) e procedi solo su una decisione firmata vincolata al payload esatto —
cambia l'azione dopo e la firma non corrisponderà più. Cosignet è un
layer di approvazione e prova, non un esecutore o un motore di policy. Funziona ovunque con
fetch globale + Web Crypto: Node 18+, Cloudflare Workers, Deno e browser.
Stato: accesso anticipato. Pubblicato su npm come
@cosignet/sdk.
npm install @cosignet/sdk
import { Cosignet } from '@cosignet/sdk';
const cosignet = new Cosignet({ apiKey: process.env.COSIGNET_API_KEY! });
const decision = await cosignet.requestApproval(
{
username: 'alex',
action: 'Bonifico bancario al fornitore',
payload: { to: 'acct_8821', amount_usd: 4200, memo: 'INV-2025-118' },
notify: 'telegram_or_email',
},
{ onCreated: (c) => console.log('Approva qui:', c.url) },
);
if (decision.status === 'approved') {
// procedi — decision.rawAssertion è la prova firmata
} else {
// 'rejected' | 'expired' | 'pending' (timeout)
}
requestApproval esegue un long-polling sulla tua connessione in uscita (~25s di hop), quindi funziona
da strumenti CLI e VPC bloccati dietro NAT/firewall — nessun webhook in entrata, porta aperta o IP pubblico richiesti.
Se il tuo sistema non può ottenere un'approvazione firmata, l'azione bloccata non deve essere eseguita. L'indisponibilità non deve mai degenerare in un'auto-approvazione. Tratta ogni timeout, errore di rete e 5xx come "non approvato".
Note di implementazione:
Vedi Quando Cosignet non è disponibile per la guida completa su disponibilità e break-glass.
const created = await cosignet.createConfirmation({ username, action, payload });
const status = await cosignet.getConfirmation(created.id, { wait: 25 }); // long-poll
Passa idempotencyKey (inviato come header Idempotency-Key) in modo che un create riprovato —
ad esempio dopo un'interruzione di rete che interrompe un long-poll — con la stessa chiave e stessa
action/payload restituisca la conferma originale (idempotent: true) invece di crearne
una duplicata o di ri-notificare l'approvatore. Riutilizzare una chiave con parametri diversi
viene rifiutato con un 422.
await cosignet.createConfirmation({
username: 'alex',
action: 'Bonifico bancario al fornitore',
payload: { to: 'acct_8821', amount_usd: 4200 },
idempotencyKey: 'wire-INV-2025-118', // stabile per operazione logica
});
Passa returnUrl per mostrare un pulsante "Torna a <host>" sulla pagina di approvazione
una volta che la richiesta viene risolta (approvata / rifiutata / scaduta) — utile per permettere a un umano di
tornare alla tua app per riattivare un'azione che è scaduta. Deve essere un URL https
e viene solo renderizzato come link cliccabile (mai recuperato lato server).
await cosignet.requestApproval({
username: 'alex',
action: 'Bonifico bancario al fornitore',
payload: { to: 'acct_8821', amount_usd: 4200 },
returnUrl: 'https://app.example.com/approvals',
});
notify ('none' | 'telegram' | 'email' | 'telegram_or_email') controlla il
ping personale al firmatario specifico:
telegram — un DM al Telegram collegato dell'approvatore (telegram_or_email preferisce
questo quando l'approvatore ha collegato una chat).email — un'email all'indirizzo dell'approvatore. Priorità: l'email diretta
che hai impostato per l'approvatore nel dashboard (solo consegna), poi la sua email di membro
verificata, poi l'email di contatto dell'account come ultima risorsa.telegram_or_email — Telegram se collegato, altrimenti l'email sopra.none — nessun ping personale.Separatamente, la portata del team è una trasmissione Slack a un canale condiviso, configurata
per account nel dashboard. È additiva e si attiva sempre quando configurata,
indipendentemente da notify (quindi notify:'none' posta comunque su Slack). Gli approvatori
impostano il loro Telegram e l'email dalla sezione Approvatori del dashboard.
Nota sulla rivelazione
public: true: l'email impegnata nel log di trasparenza è l'email verificata designata dell'account (scelta nelle impostazioni del dashboard, come parte responsabile — non il firmatario individuale). L'email di notifica diretta è solo per la consegna e non viene mai utilizzata per l'hash di rivelazione.
Ogni canale è solo link: la notifica porta l'URL di approvazione e mai
l'action o il payload. L'approvazione richiede sempre la passkey vincolata al payload
dell'approvatore sulla pagina di conferma — non esiste approva-in-canale.
verifyWebhookSignature ricalcola l'HMAC-SHA256 esadecimale del body della richiesta
grezza e lo confronta in tempo costante con l'header Cosignet-Signature. Passa
esattamente i byte che hai ricevuto — re-serializzare JSON cambia la firma.
import { verifyWebhookSignature } from '@cosignet/sdk';
const ok = await verifyWebhookSignature({
body: rawBody, // stringa grezza, non JSON ri-parsato
signature: req.headers['cosignet-signature'],
secret: process.env.COSIGNET_WEBHOOK_SECRET!,
timestamp: req.headers['cosignet-timestamp'], // protezione opzionale dal replay
toleranceSeconds: 300, // opzionale
});
if (!ok) return res.status(401).end();
Vedi docs/verification.md per la checklist lato chiamante: procedi solo su approved, confronta la decisione con l'operazione che stai per eseguire e tratta rifiutato/scaduto/timeout come stop assoluti.
new Cosignet({ apiKey, baseUrl?, fetch? })createConfirmation(input) → CreatedConfirmationgetConfirmation(id, { wait? }) → ConfirmationrequestApproval(input, { timeoutMs?, onCreated? }) → ConfirmationverifyWebhookSignature({ body, signature, secret, timestamp?, toleranceSeconds? }) → Promise<boolean>Per default il log di trasparenza trattiene il payload e non espone alcuna identità
dell'approvatore. Imposta public: true su createConfirmation/requestApproval per abilitare
una singola approvazione alla rivelazione pubblica: la sua action + payload grezzi
diventano leggibili nel bundle di verifica, e un hash PBKDF2 dell'email verificata
designata dell'account (scelta nelle impostazioni del dashboard — la parte responsabile,
non il firmatario individuale) viene impegnato nella foglia Merkle in modo che chiunque possa
verificare un indirizzo candidato.
await cosignet.requestApproval({
username: 'alice',
action: 'Pubblica risoluzione del consiglio Q3',
payload: { docId: 'res-2026-Q3' },
public: true, // permanente, irreversibile — vedi avvertenze sotto
});
examples/ — esempi copia-incolla per cURL, Node, Python, GitLab CI, MCP/tool-wrapper e Worker.security/ — vincolo del payload, modello di prova WebAuthn, modello di minaccia, log di trasparenza e divulgazione responsabile.Blocca azioni ad alto rischio dietro un'approvazione passkey:
recipes/ai-agent-gate — metti in pausa un agente AI di codifica (Claude Code) prima che esegua un comando shell pericoloso fino a quando un umano non lo co-firma.recipes/cli-approval — avvolgi qualsiasi comando (es. terraform destroy).recipes/ci-cd — blocca una pipeline (GitHub Actions / GitLab CI) fino all'approvazione.recipes/backend-service — metti in pausa un'operazione irreversibile in un servizio.[email protected]; non aprire issue pubbliche per vulnerabilità.[email protected].MIT © Cosignet
| Segnale | Significato | La tua azione |
|---|
status: approved (la firma verifica) | L'umano ha approvato questo payload esatto | Procedi |
status: rejected | L'umano ha rifiutato | Non eseguire; comunica al richiedente |
status: expired | Nessuno ha deciso in tempo | Non eseguire; richiedi di nuovo se ancora necessario |
| Timeout, errore di rete o 5xx | Stato sconosciuto | Non eseguire; riprova con backoff; allerta dopo N fallimenti |