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.ts complet).
  • Refus par défaut : une panne refuse.
  • Node 18+ (fetch global), avec un repli undici paresseux 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