@reqvet-sdk/sdk 2.2.3 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +81 -17
  2. package/SDK_REFERENCE.md +123 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -186,43 +186,107 @@ Voir [SDK_REFERENCE.md §6](./SDK_REFERENCE.md#6-webhook-events) pour la structu
186
186
 
187
187
  ## Intégration revendeur (multi-tenant)
188
188
 
189
- Si vous êtes un éditeur logiciel intégrant ReqVet pour vos clients (cliniques), utilisez une clé API revendeur pour provisionner et gérer les organisations de manière programmatique.
189
+ Si vous êtes un éditeur logiciel intégrant ReqVet pour vos clients (cliniques), vous disposez de **deux types de clés** :
190
+
191
+ | Clé | Variable | Usage |
192
+ |-----|----------|-------|
193
+ | Clé revendeur | `REQVET_RESELLER_KEY` | Provisionner et administrer les cliniques — **jamais** pour générer des jobs |
194
+ | Clé clinique | stockée dans votre DB | Faire les appels de génération pour cette clinique spécifique |
195
+
196
+ ### 1 — Onboarding : provisionner une clinique
197
+
198
+ Appelez `createOrganization` à chaque fois qu'une nouvelle clinique souscrit à votre service. Utilisez votre identifiant interne comme `externalId` — c'est le pont entre vos deux systèmes.
190
199
 
191
200
  ```ts
192
201
  import ReqVet from '@reqvet-sdk/sdk';
193
202
 
194
- // Instancier avec la clé revendeur (role='reseller')
195
203
  const reseller = new ReqVet(process.env.REQVET_RESELLER_KEY!);
196
204
 
197
- // Provisionner une clinique à l'onboarding
198
205
  const result = await reseller.createOrganization({
199
206
  name: 'Clinique du Parc',
200
207
  contactEmail: 'contact@clinique-du-parc.fr',
201
- externalId: 'votre_id_interne_4892', // votre ID garantit l'idempotence
208
+ externalId: String(clinic.id), // votre ID interne idempotence garantie
202
209
  monthlyQuota: 500,
203
210
  webhookUrl: 'https://votre-app.com/webhooks/reqvet',
204
211
  });
205
212
 
206
- // ⚠️ Stocker immédiatement ces valeurs — elles ne sont retournées qu'une seule fois
207
- await db.saveClinicCredentials({
208
- clinicId: result.organization.id,
209
- apiKey: result.api_key, // rqv_live_...
210
- webhookSecret: result.webhook_secret, // whsec_...
211
- });
213
+ if (result.api_key) {
214
+ // Première création — stocker immédiatement, chiffré au repos
215
+ // api_key et webhook_secret ne sont retournés qu'une seule fois
216
+ await db.clinics.update(clinic.id, {
217
+ reqvet_org_id: result.organization.id,
218
+ reqvet_api_key: encrypt(result.api_key), // rqv_live_...
219
+ reqvet_wh_secret: encrypt(result.webhook_secret), // whsec_...
220
+ });
221
+ }
222
+ // Si !result.api_key → clinique déjà provisionnée (idempotent), clé déjà en base
223
+ ```
212
224
 
213
- // La clinique utilise ensuite son propre client ReqVet avec sa clé
214
- const clinic = new ReqVet(result.api_key);
215
- const { system } = await clinic.listTemplates();
216
- const job = await clinic.createJob({ audioFile, animalName, templateId: system[0].id });
225
+ ### 2 Runtime : appeler ReqVet pour le bon utilisateur
217
226
 
227
+ À chaque consultation, récupérez la clé de **la clinique de l'utilisateur connecté** et faites l'appel côté serveur. La clé ne sort jamais du backend.
228
+
229
+ ```ts
230
+ // Handler server-side — appelé quand un vétérinaire lance une transcription
231
+ async function transcribe(userId: string, audioBuffer: Buffer) {
232
+ // Identifier la clinique de l'utilisateur dans VOTRE système
233
+ const clinic = await db.getClinicByUser(userId);
234
+
235
+ // Déchiffrer la clé de cette clinique — côté serveur uniquement
236
+ const reqvet = new ReqVet(decrypt(clinic.reqvet_api_key));
237
+
238
+ // Upload direct Supabase (pas de limite de taille)
239
+ const { uploadUrl, path } = await reqvet.getSignedUploadUrl('consultation.webm', 'audio/webm');
240
+ await fetch(uploadUrl, { method: 'PUT', body: audioBuffer });
241
+
242
+ // Injecter clinicId dans metadata → permet de router les webhooks entrants
243
+ return reqvet.createJob({
244
+ audioFile: path,
245
+ animalName: consultation.animalName,
246
+ templateId: clinic.reqvet_template_id,
247
+ callbackUrl: 'https://votre-app.com/webhooks/reqvet',
248
+ metadata: { clinicId: clinic.id, userId },
249
+ });
250
+ }
251
+ ```
252
+
253
+ ### 3 — Webhooks entrants : router vers la bonne clinique
254
+
255
+ Vos webhooks arrivent sur un seul endpoint. Utilisez `metadata.clinicId` (injecté au `createJob`) pour identifier la clinique et vérifier la signature avec son propre secret.
256
+
257
+ ```ts
258
+ export async function POST(req: NextRequest) {
259
+ const rawBody = await req.text();
260
+ const event = JSON.parse(rawBody);
261
+
262
+ const clinic = await db.getClinic(event.metadata?.clinicId);
263
+
264
+ const { ok } = verifyWebhookSignature({
265
+ secret: decrypt(clinic.reqvet_wh_secret), // secret de CETTE clinique
266
+ rawBody,
267
+ signature: req.headers.get('x-reqvet-signature') ?? '',
268
+ timestamp: req.headers.get('x-reqvet-timestamp') ?? '',
269
+ });
270
+
271
+ if (!ok) return new Response('Unauthorized', { status: 401 });
272
+
273
+ if (event.event === 'job.completed') {
274
+ await db.saveReport(clinic.id, event.job_id, event.html, event.fields);
275
+ }
276
+ }
277
+ ```
278
+
279
+ ### 4 — Administration : quotas, suspension, monitoring
280
+
281
+ ```ts
218
282
  // Lister toutes les cliniques avec leur usage du mois
219
283
  const { organizations } = await reseller.listOrganizations();
220
284
  // [{ id, name, is_active, monthly_quota, usage: { jobs_this_month, quota_remaining } }]
221
285
 
222
- // Modifier le quota d'une clinique
286
+ // Modifier le quota (ex. changement de plan)
223
287
  await reseller.updateOrganization(orgId, { monthlyQuota: 1000 });
224
288
 
225
- // Suspendre une clinique (révoque aussi ses clés API)
289
+ // Suspendre une clinique révoque ses clés API en cascade
226
290
  await reseller.updateOrganization(orgId, { isActive: false });
227
291
 
228
292
  // Réactiver
@@ -232,7 +296,7 @@ await reseller.updateOrganization(orgId, { isActive: true });
232
296
  await reseller.deactivateOrganization(orgId);
233
297
  ```
234
298
 
235
- > **Idempotence** : si `createOrganization` est appelé plusieurs fois avec le même `externalId`, l'organisation existante est retournée sans créer de doublon. Utile pour rendre votre processus d'onboarding sûr en cas de retry.
299
+ > Voir [SDK_REFERENCE.md §9](./SDK_REFERENCE.md#pattern--gestion-des-clés-par-clinique) pour le schéma DB complet, les règles impératives et les cas limites (rotation de clé, etc.).
236
300
 
237
301
  ## TypeScript
238
302
 
package/SDK_REFERENCE.md CHANGED
@@ -578,6 +578,123 @@ Ces méthodes sont réservées aux revendeurs (éditeurs logiciels) qui provisio
578
578
 
579
579
  ---
580
580
 
581
+ ### Pattern : gestion des clés par clinique
582
+
583
+ Chaque clinique provisionnée reçoit sa propre clé API `rqv_live_...`. Avec 100 cliniques, il faut un mécanisme pour associer la bonne clé au bon utilisateur authentifié au moment de l'appel.
584
+
585
+ **Le principe** : vous stockez les clés dans votre propre base de données, liées à vos enregistrements cliniques. Vos appels vers ReqVet se font **server-side** — la clé ne passe jamais dans le navigateur.
586
+
587
+ #### Schéma recommandé (côté revendeur)
588
+
589
+ ```sql
590
+ -- Dans votre base de données
591
+ ALTER TABLE clinics ADD COLUMN reqvet_org_id TEXT; -- UUID ReqVet
592
+ ALTER TABLE clinics ADD COLUMN reqvet_api_key TEXT; -- rqv_live_... chiffré
593
+ ALTER TABLE clinics ADD COLUMN reqvet_wh_secret TEXT; -- whsec_... chiffré
594
+ ```
595
+
596
+ > Chiffrez `reqvet_api_key` et `reqvet_wh_secret` au repos (AES-256 ou équivalent). Ces valeurs sont des secrets d'accès — traitez-les comme des mots de passe.
597
+
598
+ #### Flux d'onboarding d'une clinique
599
+
600
+ ```ts
601
+ // Appelé quand vous créez une nouvelle clinique dans votre système
602
+ async function onboardClinic(clinicId: string, clinicData: ClinicInput) {
603
+ const reseller = new ReqVet(process.env.REQVET_RESELLER_KEY!);
604
+
605
+ // externalId = votre ID interne → garantit l'idempotence en cas de retry
606
+ const result = await reseller.createOrganization({
607
+ name: clinicData.name,
608
+ contactEmail: clinicData.email,
609
+ externalId: clinicId, // ← votre identifiant interne comme pont
610
+ monthlyQuota: 200,
611
+ webhookUrl: `https://votre-app.com/webhooks/reqvet`,
612
+ });
613
+
614
+ if (result.api_key) {
615
+ // Première création : stocker la clé chiffrée
616
+ await db.clinics.update(clinicId, {
617
+ reqvet_org_id: result.organization.id,
618
+ reqvet_api_key: encrypt(result.api_key),
619
+ reqvet_wh_secret: encrypt(result.webhook_secret),
620
+ });
621
+ }
622
+ // Si !result.api_key → organisation déjà existante (idempotent), clé déjà en base
623
+ }
624
+ ```
625
+
626
+ #### Flux d'appel au moment de la consultation
627
+
628
+ ```ts
629
+ // Appelé quand un vétérinaire authentifié lance une transcription
630
+ async function transcribe(userId: string, audioBuffer: Buffer) {
631
+ // 1. Identifier la clinique de l'utilisateur dans VOTRE système
632
+ const clinic = await db.getClinicByUser(userId);
633
+
634
+ // 2. Récupérer et déchiffrer la clé ReqVet — côté serveur uniquement
635
+ const reqvetKey = decrypt(clinic.reqvet_api_key);
636
+
637
+ // 3. Instancier le client avec la clé de CETTE clinique
638
+ const reqvet = new ReqVet(reqvetKey);
639
+
640
+ // 4. Appel ReqVet — la clé ne sort jamais du serveur
641
+ const { uploadUrl, path } = await reqvet.getSignedUploadUrl('consultation.webm', 'audio/webm');
642
+ await fetch(uploadUrl, { method: 'PUT', body: audioBuffer });
643
+
644
+ const job = await reqvet.createJob({
645
+ audioFile: path,
646
+ animalName: consultation.animalName,
647
+ templateId: clinic.reqvet_template_id,
648
+ callbackUrl: `https://votre-app.com/webhooks/reqvet`,
649
+ metadata: { clinicId: clinic.id, userId },
650
+ });
651
+
652
+ return job;
653
+ }
654
+ ```
655
+
656
+ #### Réception du webhook
657
+
658
+ Le webhook ReqVet arrive sur votre endpoint. Pour l'associer à la bonne clinique, utilisez le `metadata.clinicId` que vous avez injecté lors du `createJob` :
659
+
660
+ ```ts
661
+ export async function POST(req: NextRequest) {
662
+ const rawBody = await req.text();
663
+ const event = JSON.parse(rawBody);
664
+
665
+ // Retrouver la clinique via les metadata passthrough
666
+ const clinicId = event.metadata?.clinicId;
667
+ const clinic = await db.getClinic(clinicId);
668
+
669
+ // Vérifier la signature avec le secret de CETTE clinique
670
+ const { ok } = verifyWebhookSignature({
671
+ secret: decrypt(clinic.reqvet_wh_secret),
672
+ rawBody,
673
+ signature: req.headers.get('x-reqvet-signature') ?? '',
674
+ timestamp: req.headers.get('x-reqvet-timestamp') ?? '',
675
+ });
676
+
677
+ if (!ok) return new Response('Unauthorized', { status: 401 });
678
+
679
+ // Traiter l'événement
680
+ if (event.event === 'job.completed') {
681
+ await db.saveReport(clinicId, event.job_id, event.html, event.fields);
682
+ }
683
+ }
684
+ ```
685
+
686
+ #### Règles impératives
687
+
688
+ | Règle | Détail |
689
+ |-------|--------|
690
+ | **Server-side uniquement** | La clé clinique ne doit jamais être envoyée au navigateur, ni apparaître dans les réponses API de votre frontend |
691
+ | **Chiffrement au repos** | `reqvet_api_key` et `reqvet_wh_secret` chiffrés en base — pas en clair |
692
+ | **`externalId` = votre ID** | Utilisez systématiquement votre identifiant interne comme `externalId` — c'est le pont entre vos deux systèmes et le garde-fou anti-doublon |
693
+ | **Une clé par clinique** | Ne réutilisez jamais la clé d'une clinique pour une autre — l'isolation des données ReqVet est par `org_id` |
694
+ | **Rotation** | Si une clé est compromise, désactivez la clinique (`isActive: false`), puis réactivez — les nouvelles clés devront être provisionnées manuellement via `createOrganization` |
695
+
696
+ ---
697
+
581
698
  ### `listOrganizations()`
582
699
 
583
700
  Lister toutes les organisations (cliniques) provisionnées par le revendeur, enrichies de l'usage du mois courant.
@@ -804,7 +921,11 @@ await reseller.deactivateOrganization(orgId);
804
921
  **Intégration revendeur (multi-tenant)**
805
922
 
806
923
  - [ ] Clé revendeur (`REQVET_RESELLER_KEY`) distincte et stockée séparément des clés cliniques
807
- - [ ] `externalId` systématiquement fourni à `createOrganization` pour garantir l'idempotence des onboardings
808
- - [ ] `api_key` et `webhook_secret` retournés par `createOrganization` stockés immédiatement et de façon sécurisée — ils ne sont affichés qu'une seule fois
924
+ - [ ] `externalId` systématiquement fourni à `createOrganization` valeur = votre ID interne de clinique
925
+ - [ ] `api_key` et `webhook_secret` retournés par `createOrganization` stockés immédiatement, **chiffrés au repos** — ils ne sont affichés qu'une seule fois
926
+ - [ ] Clés cliniques appelées **server-side uniquement** — jamais exposées au navigateur ni aux réponses frontend
927
+ - [ ] Un client `ReqVet` instancié avec la clé de **la clinique concernée** à chaque appel — jamais avec la clé revendeur pour les jobs
928
+ - [ ] `metadata` enrichi de `clinicId` (et `userId` si pertinent) sur chaque `createJob` — permet de router les webhooks entrants vers la bonne clinique
929
+ - [ ] Signature webhook vérifiée avec le secret de **la clinique destinataire** (et non le secret revendeur)
809
930
  - [ ] Suspension de clinique gérée via `updateOrganization(orgId, { isActive: false })` (et non `deactivateOrganization` qui est irréversible)
810
931
  - [ ] Usage mensuel (`usage.jobs_this_month`, `usage.quota_remaining`) consulté via `listOrganizations()` ou `getOrganization()` pour le monitoring et la facturation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqvet-sdk/sdk",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Official JavaScript SDK for the ReqVet veterinary report generation API.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",