
Client TypeScript léger + client Python zéro-dépendance et recettes pour verrouiller les actions à haut risque derrière une approbation par clé de passe liée à une charge utile.
Client TypeScript léger, sans dépendances, pour Cosignet — approbation humaine dans la boucle pour les actions à haut risque des agents IA, avec signatures par clé d'accès liées à la charge utile.
Mettez un humain dans la boucle avant qu'une action dangereuse ne s'exécute : interrompez-la, obtenez une
approbation explicite par clé d'accès d'une personne (Face ID / Touch ID / Windows Hello / clé de
sécurité), et continuez uniquement sur une décision signée liée à la charge utile exacte —
modifiez l'action après coup et la signature ne correspond plus. Cosignet est une
couche d'approbation et de preuve, pas un exécuteur ni un moteur de règles. Fonctionne partout avec
fetch global + Web Crypto : Node 18+, Cloudflare Workers, Deno et navigateurs.
Statut : accès anticipé. Publié sur npm sous
@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: 'Virement bancaire au fournisseur',
payload: { to: 'acct_8821', amount_usd: 4200, memo: 'INV-2025-118' },
notify: 'telegram_or_email',
},
{ onCreated: (c) => console.log('Approuver ici :', c.url) },
);
if (decision.status === 'approved') {
// continuer — decision.rawAssertion est la preuve signée
} else {
// 'rejected' | 'expired' | 'pending' (délai dépassé)
}
requestApproval effectue un long-polling sur votre propre connexion sortante (~25s de bonds), donc cela fonctionne
depuis des outils en CLI et des VPC verrouillés derrière un NAT/pare-feu — aucun webhook entrant, port ouvert ou IP publique requis.
Si votre système ne peut pas obtenir une approbation signée, l'action protégée ne doit pas s'exécuter. L'indisponibilité ne doit jamais se dégrader en auto-approbation. Traitez chaque délai dépassé, erreur réseau et 5xx comme « non approuvé ».
| Signal | Signification | Votre action |
|---|---|---|
status: approved (la signature vérifie) | Humain a approuvé cette charge utile exacte | Continuer |
status: rejected | Humain a rejeté | Ne pas exécuter ; remonter au demandeur |
status: expired | Personne n'a décidé à temps | Ne pas exécuter ; redemander si encore nécessaire |
| Délai dépassé, erreur réseau ou 5xx | État inconnu | Ne pas exécuter ; réessayer avec backoff ; alerter après N échecs |
Notes d'implémentation :
Voir Quand Cosignet est indisponible pour les conseils complets sur la disponibilité et la procédure de secours.
const created = await cosignet.createConfirmation({ username, action, payload });
const status = await cosignet.getConfirmation(created.id, { wait: 25 }); // long-poll
Passez idempotencyKey (envoyé sous l'en-tête Idempotency-Key) pour qu'une création réessayée —
par exemple après un saut réseau interrompant un long-polling — avec la même clé et la même action/charge utile renvoie la confirmation originale (idempotent: true) au lieu de créer un doublon ou de re-notifier l'approbateur. Réutiliser une clé avec des paramètres différents est rejeté avec un 422.
await cosignet.createConfirmation({
username: 'alex',
action: 'Virement bancaire au fournisseur',
payload: { to: 'acct_8821', amount_usd: 4200 },
idempotencyKey: 'wire-INV-2025-118', // stable par opération logique
});
Passez returnUrl pour afficher un bouton "Revenir sur <host>" sur la page d'approbation
une fois la requête résolue (approuvée / rejetée / expirée) — pratique pour qu'un humain puisse revenir
à votre application pour redéclencher une action qui a expiré. Doit être une URL https
et n'est jamais rendue que comme un lien cliquable (jamais récupérée côté serveur).
await cosignet.requestApproval({
username: 'alex',
action: 'Virement bancaire au fournisseur',
payload: { to: 'acct_8821', amount_usd: 4200 },
returnUrl: 'https://app.example.com/approvals',
});
notify ('none' | 'telegram' | 'email' | 'telegram_or_email') contrôle la
notification personnelle vers le signataire spécifique :
telegram — un MP vers le Telegram lié de l'approbateur (telegram_or_email privilégie
ceci lorsque l'approbateur a lié un chat).email — un email vers l'adresse propre de l'approbateur. Priorité : l'email direct
que vous définissez sur l'approbateur dans le tableau de bord (livraison uniquement), puis son email vérifié de membre,
puis l'adresse de contact du compte en dernier recours.telegram_or_email — Telegram si lié, sinon l'email ci-dessus.none — aucune notification personnelle.Séparément, la portée équipe est une diffusion Slack vers un canal partagé, configurée
par compte dans le tableau de bord. Elle est additive et se déclenche toujours lorsqu'elle est configurée,
indépendamment de notify (donc notify:'none' publie toujours dans Slack). Les approbateurs voient
leur Telegram et email définis depuis la section Approbateurs du tableau de bord.
Note sur la révélation
public: true: l'email inscrit dans le journal de transparence est l'email vérifié désigné du compte (choisi dans les paramètres du tableau de bord, en tant que partie responsable — pas le signataire individuel). L'email de notification directe est destiné uniquement à la livraison et jamais utilisé pour le hash de révélation.
Chaque canal est uniquement un lien : la notification porte l'URL d'approbation et jamais
l'action ou la payload. L'approbation nécessite toujours la clé d'accès liée à la charge utile
de l'approbateur sur la page de confirmation — il n'y a pas d'approbation dans le canal.
verifyWebhookSignature recalcule le HMAC-SHA256 hexadécimal du corps brut de la requête
et le compare en temps constant à l'en-tête Cosignet-Signature. Passez les
octets exacts que vous avez reçus — ré-sérialiser le JSON modifie la signature.
import { verifyWebhookSignature } from '@cosignet/sdk';
const ok = await verifyWebhookSignature({
body: rawBody, // chaîne brute, pas du JSON re-parsé
signature: req.headers['cosignet-signature'],
secret: process.env.COSIGNET_WEBHOOK_SECRET!,
timestamp: req.headers['cosignet-timestamp'], // protection optionnelle contre le rejeu
toleranceSeconds: 300, // optionnel
});
if (!ok) return res.status(401).end();