SDK TypeScript
@agreely/sdk est le client Node fin et entièrement typé pour l'
API /v1. Un seul appel protège un usage de données par une
vérification de consentement en direct et faisant autorité. Il n'y a aucune copie
locale et aucune mémoire-cache : chaque check() est un appel frais, car mettre en
cache une autorisation pendant qu'une révocation arrive est une faute d'exactitude.
- DX à un seul appel, typée de bout en bout (ESM + CJS,
.d.tscomplet). - Refus par défaut : une panne refuse.
- Node 18+ (
fetchglobal), avec un repliundiciparesseux et optionnel.
Installer
npm install @agreely/sdk
Construire
import { Agreely } from "@agreely/sdk";
const agreely = new Agreely({ apiKey: process.env.AGREELY_API_KEY! });
Toutes les options du constructeur :
| Option | Défaut | Rôle |
|---|---|---|
apiKey (requis) |
(aucun) | une valeur vide lève AgreelyConfigError |
baseUrl |
https://api.agreely.ca |
racine de l'API |
timeout |
800 (ms) |
budget total par appel |
degradeOnOutage |
(désactivé) | adhésion explicite à l'ouverture en cas de panne |
onBreakGlass |
(aucun) | collecteur d'audit (event) => void du bris de glace |
maxDegradeWindow |
"24h" |
plafond ferme du ttl de bris de glace et de la fenêtre de panne |
fetch |
fetch global |
injectez votre propre implémentation |
maxRetries |
0 |
tentatives supplémentaires sur un 429 seulement, appels idempotents |
respectRetryAfter |
true |
honore le Retry-After du serveur |
Les ressources sont des propriétés en lecture seule : agreely.consentRequests,
agreely.manualConsents, agreely.relationships, agreely.catalog,
agreely.breakGlass.
check() renvoie un booléen
allow est le seul true. Envoyez les libellés bruts lisibles; le SDK les
transmet tels quels et ne normalise jamais la catégorie ni la finalité.
if (await agreely.check("cust_8812", "Phone number", "Billing")) {
// ...you may use the phone number for billing
}
checkDetailed() renvoie la décision
const d = await agreely.checkDetailed("cust_8812", "Phone number", "Billing");
// { decision: "allow" | "deny",
// status: "active" | "none" | "revoked" | "expired" | "erased",
// consentRef?: "0x…", // absent when status is "none"
// checkedAt: "2026-…Z",
// assurance?: "citizen_signed" | "company_attested",
// degraded?: boolean, mode?: string } // present only on a degraded allow
Un refus est un 200 normal : checkDetailed le renvoie, il ne lève pas
d'exception. Seuls l'authentification, la validation, la limite de débit et les
pannes lèvent une exception. Le champ assurance distingue un consentement
signé par le citoyen (citizen_signed) d'un consentement attesté par
l'entreprise (company_attested).
Vérification par lot : checkBatch() et checkFields()
Pour évaluer plusieurs cellules en un seul aller-retour au lieu de N appels à
check(). Les décisions renvoyées sont alignées sur les items soumis. Les
category / purpose partent bruts (le serveur normalise). En cas de panne,
ces méthodes lèvent AgreelyUnavailableError (pas de dégradation ici).
const decisions = await agreely.checkBatch([
{ customerRef: "cust_8812", category: "Phone number", purpose: "Billing" },
{ customerRef: "cust_8813", category: "Email address", purpose: "Newsletter" },
]);
// BatchDecision[]: { customerRef, category, purpose, decision, status,
// consentRef?, assurance?, checkedAt } // consentRef/assurance absent for status "none"
// checkBatch([]) court-circuite vers [] côté client, sans aucun aller-retour.
checkFields() est la forme ergonomique en produit cartésien : donnez une liste
de clients et une liste de champs, elle construit toutes les paires, appelle
checkBatch() une fois, et renvoie une barrière isAllowed prête à l'emploi.
const grid = await agreely.checkFields(
["cust_8812", "cust_8813"],
[ { category: "Phone number", purpose: "Billing" },
{ category: "Email address", purpose: "Newsletter" } ],
);
// CheckFieldsResult: { decisions, isAllowed(customerRef, category, purpose): boolean }
if (grid.isAllowed("cust_8812", "Phone number", "Billing")) {
// ...you may display this field for this customer
}
Cas d'usage : protéger l'affichage d'une liste de clients
Afficher un tableau de N clients x M champs est N x M usages de renseignements personnels : sous la Loi 25, chaque champ affiché est un usage encadré par une vérification (limitation de l'usage, art. 12; responsabilité, art. 3.1). checkFields regroupe la grille entière en un seul aller-retour et vous rend un isAllowed(customerRef, category, purpose) à appeler par cellule. Le regroupement est une optimisation de la même responsabilité par décision (chaque cellule reste décidée et journalisée par le serveur), pas un affaiblissement.
Identité de la clé
identity() interroge GET /v1/whoami et renvoie les portées que porte
réellement la clé, vérifiées côté serveur (n'importe quelle portée y accède).
La réponse est à divulgation minimale : uniquement les portées.
const me = await agreely.identity();
// Identity: { scopes: ("check" | "issue" | "attest" | "relationship")[] }
if (me.scopes.includes("issue")) {
// ...this key may issue consent requests
}
// The configured endpoint is also exposed client-side:
agreely.baseUrl; // "https://api.agreely.ca"
Demandes de consentement
create envoie un courriel au destinataire; les items sont des identifiants de
catalogue ou des objets {category, purpose}.
const r = await agreely.consentRequests.create({
customerId: "cust_8812",
recipientEmail: "[email protected]",
items: ["<catalogEntryId>", { category: "Email address", purpose: "Newsletter" }],
validUntil: "2031-01-01",
});
// IssuedRequest: { requestId: "0x…64hex", status: "pending", deepLink, emailDelivered, items }
// List (cursor-paged) / get by protocol requestId.
const page = await agreely.consentRequests.list({ status: "pending", cursor });
// { items, nextCursor }, nextCursor is null when exhausted
const one = await agreely.consentRequests.get("0x…"); // the requestId, NOT a uuid
// ConsentRequestRecord: { requestId, status: "pending"|"approved"|"refused"|"expired"|"revoked_before_action",
// validUntil, expiresAt, createdAt, settledAt (nullable), items }
create n'est jamais ré-essayé automatiquement (il envoie un courriel). Le SDK
attache un Idempotency-Key unique par appel; passez le vôtre pour qu'une nouvelle
tentative rejoue l'original :
await agreely.consentRequests.create(input, { idempotencyKey: "order-4471" });
Parcourir toutes les pages, et attendre le règlement
listAll est un générateur asynchrone (défaut maxPages 1000); collect réunit
tout en un tableau; waitForSettlement interroge get jusqu'à ce que la demande
soit réglée.
for await (const rec of agreely.consentRequests.listAll({ status: "pending" })) {
// one ConsentRequestRecord at a time
}
const all = await agreely.consentRequests.collect({ status: "pending" });
// Poll until settled. Defaults: intervalMs 2000, timeoutMs 120000.
const settled = await agreely.consentRequests.waitForSettlement("0x…", {
intervalMs: 2000,
timeoutMs: 120000,
signal: controller.signal, // TS only: an AbortSignal
});
waitForSettlement lève AgreelyTimeoutError si le budget client s'écoule (ce
n'est pas une panne serveur), et AgreelyAbortError si l'AbortSignal fourni
se déclenche.
Annuler une demande en attente
cancel annule une demande en attente par son requestId de protocole (la voie
« révocation avant action »). Elle est idempotente : une demande déjà terminale
n'est pas une erreur, elle renvoie cancelled: false. Un requestId inconnu lève
AgreelyNotFoundError.
const c = await agreely.consentRequests.cancel("0x…");
// CancelledRequest: { requestId, status: "revoked_before_action", cancelled: true }
// Already terminal -> { requestId, status, cancelled: false } (no error)
Consentements manuels
Pour un consentement recueilli hors ligne (un PDF signé à la main). Vous enregistrez la preuve : le hachage SHA-256 du PDF, et facultativement le PDF lui-même.
const m = await agreely.manualConsents.record({
customerId: "cust_8812",
documentVersionId: "doc_v3",
effectiveDate: "2026-01-01",
validUntil: "2031-01-01",
items: [{ category: "Phone number", purpose: "Billing" }],
evidence: {
pdfSha256: Agreely.hashPdf(bytes), // "0x"+sha256, required
pdf: base64Pdf, // optional opt-in upload
},
});
// Note: manual consents do NOT honor an Idempotency-Key (only consentRequests.create does).
// ManualConsentResult: { consentId, merkleRoot, consentRefs[] (one per cell),
// assurance: "company_attested", anchored: false } // anchored is false at record time
// A one-time link for the citizen to claim/confirm.
const link = await agreely.manualConsents.createClaimLink({ customerId: "cust_8812", reference: "order-4471" });
// { claimUrl, token, expiresAt }
// Revoke or erase by consentRef.
await agreely.manualConsents.revoke("0x…", { reason: "customer request" });
// { consentRef, revoked, alreadyRevoked }
await agreely.manualConsents.erase("0x…", { reason: "erasure request" });
// { consentRef, erased, alreadyErased }
Fin d'une relation client
Attestez, depuis votre propre code, qu'une relation client est terminée (Loi 25
art. 23). Portée relationship. Le customerRef est votre propre référence (la
même que pour check), rattachée à l'entreprise de la clé. Le reason est
requis (un motif vide lève une AgreelyError). L'appel est idempotent :
re-finir une relation déjà terminée renvoie le endedAt et l'origine endedBy
d'origine. C'est une surcouche de cycle de vie : elle ne révoque ni n'efface aucun
consentement.
const r = await agreely.relationships.end({ customerRef: "cust_8812", reason: "Compte fermé, fins accomplies" });
// RelationshipEnded: { customerRef, status: "ended", endedAt, endedBy: "company" | "citizen_request" }
Catalogue
const catalog = await agreely.catalog.list();
// CatalogEntry[]: { id, category, purpose, description (nullable) }
Hacher un PDF
hashPdf et hashPdfFile sont statiques et n'appellent aucun réseau. Elles
produisent la forme exacte attendue par evidence.pdfSha256 : "0x" + le SHA-256
en hexadécimal.
const h1 = Agreely.hashPdf(bytes); // bytes is a Uint8Array
const h2 = await Agreely.hashPdfFile("/path/to/consent.pdf");
Options de limite de débit
maxRetries (défaut 0) ajoute des tentatives supplémentaires sur un 429
seulement, et seulement pour les appels idempotents (GET et check); un
appel mutant ne ré-essaie jamais un 429. respectRetryAfter (défaut true)
honore l'en-tête Retry-After du serveur.
Erreurs typées
Chaque échec est une sous-classe d'AgreelyError (avec .code, .status,
.field). Un refus n'est jamais une erreur.
| Erreur | Quand |
|---|---|
AgreelyAuthError |
401 / 403 |
AgreelyValidationError |
400 / 422 (.field nomme l'entrée) |
AgreelyNotFoundError |
404 |
AgreelyRateLimitError |
429 (.retryAfter en secondes) |
AgreelyUnavailableError |
503 / réseau / délai ou abandon (ré-essayable; la seule erreur soumise à la politique de dégradation) |
AgreelyTimeoutError |
budget de sondage client écoulé dans waitForSettlement (code: "timeout", ce n'est pas une panne serveur) |
AgreelyAbortError |
TS seulement : l'AbortSignal de l'appelant s'est déclenché (code: "aborted") |
AgreelyConfigError |
mauvaise configuration client (levée à l'initialisation) |
import { AgreelyRateLimitError } from "@agreely/sdk";
try {
await agreely.check(id, cat, pur);
} catch (e) {
if (e instanceof AgreelyRateLimitError) await sleep((e.retryAfter ?? 1) * 1000);
else throw e;
}
Le délai d'expiration par défaut est un budget total serré de 800 ms.
Refus par défaut, et l'exception à trois barrières
Lorsqu'Agreely est injoignable (503 / délai d'expiration / réseau), check()
refuse et checkDetailed() lève AgreelyUnavailableError. Un véritable
refus 200 n'est jamais touché.
Inscrire une catégorie en ouverture en cas de panne (fail-open) est possible,
mais seulement à travers trois barrières indépendantes, afin que cela ne puisse
jamais se produire par accident. Une autorisation dégradée est renvoyée comme
{ decision: "allow", status: "active", degraded: true, mode: … } sans champ
assurance : c'est une dérogation délibérée et auditée au refus par défaut.
Barrière 1 : liste d'autorisation de configuration (avec un collecteur d'audit obligatoire)
const agreely = new Agreely({
apiKey,
degradeOnOutage: {
mode: "fail-open", // the explicit word
categories: ["Browsing/usage"], // ONLY these may ever degrade
maxOutageWindow: "5m", // refuse to degrade past this
onDegrade: (ctx) => audit.log(ctx) // MANDATORY: without it the constructor throws
},
});
Barrière 2 : l'adhésion par appel
// Effective ONLY because the category is allow-listed above. Without the
// config, this per-call opt-in still denies.
await agreely.check("cust_8812", "Browsing/usage", "Analytics", { onOutage: "allow" });
Barrière 3 : bris de glace (le levier de l'exploitant)
agreely.breakGlass.engage({ reason: "incident-4471", ttl: "30m", scope: ["Browsing/usage"] });
// ...degraded checks in scope now allow, tagged breakGlass:true, until ttl expires
agreely.breakGlass.disengage();
Le bris de glace expire automatiquement, exige une reason (l'engager sans en
fournir une lève une exception), et est indépendant de la liste d'autorisation de
configuration. C'est une capacité propre au SDK TypeScript : le SDK PHP, à
portée de requête, s'arrête à deux barrières.
Aucune mémoire-cache d'autorisations, et des fenêtres bornées
Il n'y a aucune mémoire-cache des décisions d'autorisation, par conception. Chaque autorisation dégradée émet un enregistrement de preuve via onDegrade, et chaque autorisation accordée par bris de glace émet un événement onBreakGlass. Le maxDegradeWindow du client (défaut 24 h) plafonne à la fois le ttl d'un bris de glace et la fenêtre de panne; une valeur au-delà du plafond lève AgreelyConfigError plutôt que d'ouvrir une fenêtre d'ouverture en cas de panne non bornée.
Une { onOutage: "allow" } par appel qui n'est pas adossée à une entrée
degradeOnOutage.categories correspondante n'a aucun effet : la vérification
refuse toujours (et le SDK consigne un avertissement de développement unique,
réduit au silence avec AGREELY_SILENCE_WARNINGS).
Vérifier un ConsentReceipt
Agreely.verifyReceipt est statique, ne demande aucune clé d'API, et
résout toujours : un reçu falsifié rend un verdict, jamais une exception. Elle
distingue une vérification qui a échoué (fail) d'une vérification qui n'a pas pu
aboutir (unavailable), pour qu'un reçu valide pendant une panne DID ne ressemble
jamais à une contrefaçon.
const v = await Agreely.verifyReceipt(receipt, { rpcUrl: process.env.BASE_SEPOLIA_RPC });
v.overall; // "verified" | "partial" | "failed" | "unavailable"
La page dédiée détaille les quatre contrôles, le tableau ce qui est prouvé
contre ce qui est présumé, et pourquoi un reçu citoyen ne peut jamais être
verified hors ligne :
Ensuite
- Composer avec les pannes parcourt en profondeur la décision refus par défaut contre ouverture en cas de panne.
- SDK PHP et CLI pour la même surface ailleurs.