@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.
- package/README.md +81 -17
- package/SDK_REFERENCE.md +123 -2
- 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),
|
|
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:
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
286
|
+
// Modifier le quota (ex. changement de plan)
|
|
223
287
|
await reseller.updateOrganization(orgId, { monthlyQuota: 1000 });
|
|
224
288
|
|
|
225
|
-
// Suspendre une clinique
|
|
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
|
-
>
|
|
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`
|
|
808
|
-
- [ ] `api_key` et `webhook_secret` retournés par `createOrganization` stockés immédiatement
|
|
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
|