@reqvet-sdk/sdk 2.2.1 → 2.2.3
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/CHANGELOG.md +8 -0
- package/README.md +95 -5
- package/SDK_REFERENCE.md +338 -44
- package/SECURITY.md +56 -2
- package/package.json +1 -1
- package/src/index.d.ts +89 -1
- package/src/index.js +109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.1
|
|
4
|
+
|
|
5
|
+
- **Feat** : ajout de `getSignedUploadUrl(fileName, contentType)` — obtenir une URL presignée Supabase pour uploader l'audio directement, sans passer par `/api/v1/upload` (Vercel Serverless Function, limite ~4.5 MB). Recommandé pour les proxies serveur (Next.js, Express…) gérant des fichiers > 4 MB.
|
|
6
|
+
- **Types** : ajout de `SignedUploadResult` dans `index.d.ts`.
|
|
7
|
+
- **Docs** : `uploadAudio()` annotée avec l'avertissement Vercel dans `index.js`, `index.d.ts`, `SDK_REFERENCE.md` et `README.md`.
|
|
8
|
+
- **Examples** : `nextjs/route-generate.ts` et `nextjs/route-generate.mjs` mis à jour pour utiliser `getSignedUploadUrl()`.
|
|
9
|
+
- **Security** : exemple proxy dans `SECURITY.md` mis à jour.
|
|
10
|
+
|
|
3
11
|
## 2.2.0
|
|
4
12
|
|
|
5
13
|
- **Feat** : ajout de `listJobs(options?)` — liste les jobs avec pagination (`limit`, `offset`) et filtres (`status`, `sort`, `order`). Aligne le SDK sur `GET /api/v1/jobs`.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ SDK JavaScript/TypeScript officiel pour l'API [ReqVet](https://reqvet.com) — g
|
|
|
16
16
|
- **Reformuler** pour une audience spécifique — propriétaire, référé, spécialiste (`reformulateReport`)
|
|
17
17
|
- **Gérer les templates** (`listTemplates`, `createTemplate`, `updateTemplate`, `deleteTemplate`)
|
|
18
18
|
- **Vérifier les webhooks** avec HMAC (`@reqvet-sdk/sdk/webhooks`)
|
|
19
|
+
- **Provisionner et gérer des cliniques** en mode revendeur multi-tenant (`createOrganization`, `listOrganizations`, `updateOrganization`, `deactivateOrganization`)
|
|
19
20
|
|
|
20
21
|
> **Note** : ce SDK n'inclut pas d'enregistreur audio. Votre application gère l'enregistrement et passe un `File`, `Blob` ou `Buffer` au SDK.
|
|
21
22
|
|
|
@@ -51,6 +52,8 @@ const templateId = system[0].id;
|
|
|
51
52
|
|
|
52
53
|
### Flux webhook (recommandé)
|
|
53
54
|
|
|
55
|
+
Pour les intégrations serveur (Next.js, Express…), utilisez `getSignedUploadUrl()` qui uploade le fichier directement dans Supabase sans passer par une Vercel Serverless Function — **pas de limite de taille**.
|
|
56
|
+
|
|
54
57
|
```ts
|
|
55
58
|
import ReqVet from '@reqvet-sdk/sdk';
|
|
56
59
|
|
|
@@ -58,20 +61,36 @@ const reqvet = new ReqVet(process.env.REQVET_API_KEY!, {
|
|
|
58
61
|
baseUrl: process.env.REQVET_BASE_URL,
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
// 1.
|
|
62
|
-
const { path } = await reqvet.
|
|
64
|
+
// 1. Obtenir une URL signée Supabase (requête JSON légère, pas de fichier)
|
|
65
|
+
const { uploadUrl, path } = await reqvet.getSignedUploadUrl(
|
|
66
|
+
'consultation.webm',
|
|
67
|
+
'audio/webm',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// 2. Uploader directement vers Supabase (contourne Vercel, pas de limite de taille)
|
|
71
|
+
await fetch(uploadUrl, {
|
|
72
|
+
method: 'PUT',
|
|
73
|
+
headers: { 'Content-Type': 'audio/webm' },
|
|
74
|
+
body: audioBuffer, // Buffer | Blob | File
|
|
75
|
+
});
|
|
63
76
|
|
|
64
|
-
//
|
|
77
|
+
// 3. Créer un job — ReqVet POSTera le résultat sur votre webhook quand il sera prêt
|
|
65
78
|
const job = await reqvet.createJob({
|
|
66
79
|
audioFile: path,
|
|
67
80
|
animalName: 'Rex',
|
|
68
81
|
templateId: 'your-template-uuid',
|
|
69
82
|
callbackUrl: 'https://your-app.com/api/reqvet/webhook',
|
|
70
|
-
metadata: { consultationId: 'abc123' },
|
|
83
|
+
metadata: { consultationId: 'abc123' },
|
|
71
84
|
});
|
|
72
85
|
// { job_id: '...', status: 'pending' }
|
|
73
86
|
```
|
|
74
87
|
|
|
88
|
+
> **`uploadAudio()` vs `getSignedUploadUrl()`**
|
|
89
|
+
>
|
|
90
|
+
> `uploadAudio()` est pratique pour des fichiers légers (< 4 MB) ou des contextes navigateur.
|
|
91
|
+
> Pour les proxies serveur, préférez `getSignedUploadUrl()` : le fichier va directement dans Supabase,
|
|
92
|
+
> sans passer par `/api/v1/upload` (Vercel Serverless Function, limite ~4.5 MB).
|
|
93
|
+
|
|
75
94
|
Votre webhook reçoit un événement `job.completed` :
|
|
76
95
|
|
|
77
96
|
```json
|
|
@@ -123,9 +142,12 @@ export async function POST(req: NextRequest) {
|
|
|
123
142
|
|
|
124
143
|
## API
|
|
125
144
|
|
|
145
|
+
### Génération de comptes rendus
|
|
146
|
+
|
|
126
147
|
| Méthode | Description |
|
|
127
148
|
|---------|-------------|
|
|
128
|
-
| `
|
|
149
|
+
| `getSignedUploadUrl(fileName, contentType)` | URL signée Supabase pour upload direct (recommandé serveur) |
|
|
150
|
+
| `uploadAudio(audio, fileName?)` | Uploader un fichier audio via ReqVet (limite Vercel ~4.5 MB) |
|
|
129
151
|
| `generateReport(params)` | Upload + création de job (helper tout-en-un) |
|
|
130
152
|
| `createJob(params)` | Créer un job de génération |
|
|
131
153
|
| `listJobs(options?)` | Lister les jobs avec pagination et filtre par statut |
|
|
@@ -142,6 +164,18 @@ export async function POST(req: NextRequest) {
|
|
|
142
164
|
| `deleteTemplate(templateId)` | Supprimer un template |
|
|
143
165
|
| `health()` | Vérification de l'état de l'API |
|
|
144
166
|
|
|
167
|
+
### API Partenaire / Revendeur
|
|
168
|
+
|
|
169
|
+
> Ces méthodes nécessitent une clé API revendeur (`rqv_live_...` avec `role='reseller'`), distincte de la clé d'une clinique standard. Contactez votre responsable de compte ReqVet pour obtenir une clé revendeur.
|
|
170
|
+
|
|
171
|
+
| Méthode | Description |
|
|
172
|
+
|---------|-------------|
|
|
173
|
+
| `listOrganizations()` | Lister toutes les cliniques provisionnées par le revendeur |
|
|
174
|
+
| `createOrganization(params)` | Provisionner une nouvelle clinique (génère sa clé API et son webhook secret) |
|
|
175
|
+
| `getOrganization(orgId)` | Obtenir le détail et l'usage du mois d'une clinique |
|
|
176
|
+
| `updateOrganization(orgId, updates)` | Modifier le quota, le statut ou le webhook d'une clinique |
|
|
177
|
+
| `deactivateOrganization(orgId)` | Suspendre une clinique et révoquer ses clés API (soft delete) |
|
|
178
|
+
|
|
145
179
|
## Événements webhook
|
|
146
180
|
|
|
147
181
|
ReqVet déclenche 5 types d'événements : `job.completed`, `job.failed`, `job.amended`, `job.amend_failed`, `job.regenerated`.
|
|
@@ -150,6 +184,56 @@ Les livraisons échouées sont retentées 3 fois (0s, 2s, 5s). Implémentez l'id
|
|
|
150
184
|
|
|
151
185
|
Voir [SDK_REFERENCE.md §6](./SDK_REFERENCE.md#6-webhook-events) pour la structure complète des payloads de chaque événement.
|
|
152
186
|
|
|
187
|
+
## Intégration revendeur (multi-tenant)
|
|
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.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import ReqVet from '@reqvet-sdk/sdk';
|
|
193
|
+
|
|
194
|
+
// Instancier avec la clé revendeur (role='reseller')
|
|
195
|
+
const reseller = new ReqVet(process.env.REQVET_RESELLER_KEY!);
|
|
196
|
+
|
|
197
|
+
// Provisionner une clinique à l'onboarding
|
|
198
|
+
const result = await reseller.createOrganization({
|
|
199
|
+
name: 'Clinique du Parc',
|
|
200
|
+
contactEmail: 'contact@clinique-du-parc.fr',
|
|
201
|
+
externalId: 'votre_id_interne_4892', // votre ID — garantit l'idempotence
|
|
202
|
+
monthlyQuota: 500,
|
|
203
|
+
webhookUrl: 'https://votre-app.com/webhooks/reqvet',
|
|
204
|
+
});
|
|
205
|
+
|
|
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
|
+
});
|
|
212
|
+
|
|
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 });
|
|
217
|
+
|
|
218
|
+
// Lister toutes les cliniques avec leur usage du mois
|
|
219
|
+
const { organizations } = await reseller.listOrganizations();
|
|
220
|
+
// [{ id, name, is_active, monthly_quota, usage: { jobs_this_month, quota_remaining } }]
|
|
221
|
+
|
|
222
|
+
// Modifier le quota d'une clinique
|
|
223
|
+
await reseller.updateOrganization(orgId, { monthlyQuota: 1000 });
|
|
224
|
+
|
|
225
|
+
// Suspendre une clinique (révoque aussi ses clés API)
|
|
226
|
+
await reseller.updateOrganization(orgId, { isActive: false });
|
|
227
|
+
|
|
228
|
+
// Réactiver
|
|
229
|
+
await reseller.updateOrganization(orgId, { isActive: true });
|
|
230
|
+
|
|
231
|
+
// Désactiver définitivement (soft delete — données conservées pour le RGPD)
|
|
232
|
+
await reseller.deactivateOrganization(orgId);
|
|
233
|
+
```
|
|
234
|
+
|
|
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.
|
|
236
|
+
|
|
153
237
|
## TypeScript
|
|
154
238
|
|
|
155
239
|
Définitions TypeScript complètes incluses :
|
|
@@ -162,6 +246,12 @@ import type {
|
|
|
162
246
|
Template,
|
|
163
247
|
ReqVetReformulation,
|
|
164
248
|
ExtractedFields,
|
|
249
|
+
// Partner / Reseller
|
|
250
|
+
PartnerOrganization,
|
|
251
|
+
OrganizationUsage,
|
|
252
|
+
CreateOrganizationParams,
|
|
253
|
+
UpdateOrganizationParams,
|
|
254
|
+
CreateOrganizationResult,
|
|
165
255
|
} from '@reqvet-sdk/sdk';
|
|
166
256
|
```
|
|
167
257
|
|
package/SDK_REFERENCE.md
CHANGED
|
@@ -11,8 +11,8 @@ import ReqVet from '@reqvet-sdk/sdk';
|
|
|
11
11
|
|
|
12
12
|
const reqvet = new ReqVet(process.env.REQVET_API_KEY!, {
|
|
13
13
|
baseUrl: process.env.REQVET_BASE_URL ?? 'https://api.reqvet.com',
|
|
14
|
-
pollInterval: 5000,
|
|
15
|
-
timeout: 5 * 60 * 1000,
|
|
14
|
+
pollInterval: 5000, // intervalle de polling en ms (défaut : 5000)
|
|
15
|
+
timeout: 5 * 60 * 1000, // attente maximale en polling en ms (défaut : 300 000 = 5 min)
|
|
16
16
|
});
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -25,6 +25,7 @@ La clé API doit commencer par `rqv_`. Une `Error` est levée immédiatement dan
|
|
|
25
25
|
### Obtenir vos identifiants
|
|
26
26
|
|
|
27
27
|
Votre responsable de compte ReqVet vous fournira :
|
|
28
|
+
|
|
28
29
|
- `REQVET_API_KEY` — votre clé API d'organisation (`rqv_live_...`)
|
|
29
30
|
- `REQVET_BASE_URL` — l'URL de base de l'API
|
|
30
31
|
- `REQVET_WEBHOOK_SECRET` — votre secret de signature webhook (si vous utilisez les webhooks)
|
|
@@ -68,9 +69,53 @@ const report = await reqvet.generateReport({ audio, animalName, templateId, wait
|
|
|
68
69
|
|
|
69
70
|
## 4) Méthodes
|
|
70
71
|
|
|
72
|
+
### `getSignedUploadUrl(fileName, contentType)` ⭐ recommandé serveur
|
|
73
|
+
|
|
74
|
+
Obtenir une URL signée Supabase pour uploader le fichier audio directement, sans passer par une Vercel Serverless Function.
|
|
75
|
+
|
|
76
|
+
**Quand l'utiliser :** intégrations serveur (Next.js proxy, Express, etc.), fichiers > 4 MB.
|
|
77
|
+
|
|
78
|
+
**Flow :**
|
|
79
|
+
```
|
|
80
|
+
getSignedUploadUrl() → PUT uploadUrl (Supabase direct) → createJob({ audioFile: path })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Paramètres :**
|
|
84
|
+
| Nom | Type | Requis | Description |
|
|
85
|
+
|-----|------|--------|-------------|
|
|
86
|
+
| `fileName` | `string` | ✅ | Nom du fichier (ex. `consultation.webm`) |
|
|
87
|
+
| `contentType` | `string` | ✅ | Type MIME (ex. `audio/webm`) |
|
|
88
|
+
|
|
89
|
+
**Réponse :**
|
|
90
|
+
```ts
|
|
91
|
+
{
|
|
92
|
+
uploadUrl: string; // URL presignée Supabase — à utiliser avec PUT
|
|
93
|
+
path: string; // chemin de stockage — à passer à createJob()
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Exemple :**
|
|
98
|
+
```ts
|
|
99
|
+
const { uploadUrl, path } = await reqvet.getSignedUploadUrl('consultation.webm', 'audio/webm');
|
|
100
|
+
|
|
101
|
+
await fetch(uploadUrl, {
|
|
102
|
+
method: 'PUT',
|
|
103
|
+
headers: { 'Content-Type': 'audio/webm' },
|
|
104
|
+
body: audioBuffer,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const job = await reqvet.createJob({ audioFile: path, animalName, templateId });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
71
112
|
### `uploadAudio(audio, fileName?)`
|
|
72
113
|
|
|
73
|
-
Uploader un fichier audio vers le stockage ReqVet
|
|
114
|
+
Uploader un fichier audio vers le stockage ReqVet via `/api/v1/upload`.
|
|
115
|
+
|
|
116
|
+
> ⚠️ **Limite serveur** : `/api/v1/upload` est une Vercel Serverless Function avec une limite de payload de ~4.5 MB. Pour les proxies serveur gérant des fichiers > 4 MB, utilisez [`getSignedUploadUrl()`](#getsigneduploadurlfilename-contenttype--recommandé-serveur) à la place.
|
|
117
|
+
>
|
|
118
|
+
> `uploadAudio()` reste adapté pour les contextes navigateur (Blob/File natif, pas de limite Vercel) ou les fichiers légers.
|
|
74
119
|
|
|
75
120
|
**Paramètres :**
|
|
76
121
|
| Nom | Type | Requis | Description |
|
|
@@ -79,10 +124,11 @@ Uploader un fichier audio vers le stockage ReqVet.
|
|
|
79
124
|
| `fileName` | `string` | — | Nom du fichier, utilisé pour inférer le type MIME (défaut : `audio.webm`) |
|
|
80
125
|
|
|
81
126
|
**Réponse :**
|
|
127
|
+
|
|
82
128
|
```ts
|
|
83
129
|
{
|
|
84
|
-
audio_file: string;
|
|
85
|
-
path: string;
|
|
130
|
+
audio_file: string; // chemin de stockage canonique — à passer à createJob()
|
|
131
|
+
path: string; // alias de audio_file
|
|
86
132
|
size_bytes: number;
|
|
87
133
|
content_type: string;
|
|
88
134
|
}
|
|
@@ -110,6 +156,7 @@ Wrapper pratique : `uploadAudio → createJob`. Attend optionnellement la fin du
|
|
|
110
156
|
| `onStatus` | `(status: string) => void` | — | Appelé à chaque poll (uniquement si `waitForResult: true`) |
|
|
111
157
|
|
|
112
158
|
**Réponse :**
|
|
159
|
+
|
|
113
160
|
- `waitForResult: false` (défaut) : `{ job_id: string, status: 'pending' }`
|
|
114
161
|
- `waitForResult: true` : `ReqVetReport` (voir `waitForJob`)
|
|
115
162
|
|
|
@@ -130,8 +177,12 @@ Démarrer un pipeline de transcription + génération de compte rendu.
|
|
|
130
177
|
| `extraInstructions` | `string` | — | Instructions de génération supplémentaires (max 5 000 caractères) |
|
|
131
178
|
|
|
132
179
|
**Réponse :**
|
|
180
|
+
|
|
133
181
|
```ts
|
|
134
|
-
{
|
|
182
|
+
{
|
|
183
|
+
job_id: string;
|
|
184
|
+
status: 'pending';
|
|
185
|
+
}
|
|
135
186
|
```
|
|
136
187
|
|
|
137
188
|
> **Limite de débit** : 10 000 requêtes/minute par organisation.
|
|
@@ -152,6 +203,7 @@ Lister les jobs de l'organisation authentifiée, avec pagination et filtrage.
|
|
|
152
203
|
| `order` | `string` | `desc` | Direction : `asc` ou `desc` |
|
|
153
204
|
|
|
154
205
|
**Réponse :**
|
|
206
|
+
|
|
155
207
|
```ts
|
|
156
208
|
{
|
|
157
209
|
jobs: JobSummary[];
|
|
@@ -167,21 +219,22 @@ Obtenir l'état actuel et le résultat d'un job.
|
|
|
167
219
|
|
|
168
220
|
**Champs de réponse par statut :**
|
|
169
221
|
|
|
170
|
-
| Champ
|
|
171
|
-
|
|
172
|
-
| `job_id`
|
|
173
|
-
| `status`
|
|
174
|
-
| `animal_name`
|
|
175
|
-
| `metadata`
|
|
176
|
-
| `transcription` |
|
|
177
|
-
| `result.html`
|
|
178
|
-
| `result.fields` |
|
|
179
|
-
| `cost`
|
|
180
|
-
| `error`
|
|
222
|
+
| Champ | `pending` | `transcribing` | `generating` | `completed` | `failed` |
|
|
223
|
+
| --------------- | :-------: | :------------: | :----------: | :---------: | :------: |
|
|
224
|
+
| `job_id` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
225
|
+
| `status` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
226
|
+
| `animal_name` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
227
|
+
| `metadata` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
228
|
+
| `transcription` | — | — | ✅ | ✅ | — |
|
|
229
|
+
| `result.html` | — | — | — | ✅ | — |
|
|
230
|
+
| `result.fields` | — | — | — | ✅\* | — |
|
|
231
|
+
| `cost` | — | — | — | ✅ | — |
|
|
232
|
+
| `error` | — | — | — | — | ✅ |
|
|
181
233
|
|
|
182
|
-
|
|
234
|
+
\*`result.fields` n'est présent que si votre organisation a un `field_schema` configuré (extraction de données structurées). Vaut `null` sinon. Voir [Schéma de champs](#5-schéma-de-champs) ci-dessous.
|
|
183
235
|
|
|
184
236
|
**Structure du coût (jobs terminés) :**
|
|
237
|
+
|
|
185
238
|
```ts
|
|
186
239
|
cost: {
|
|
187
240
|
transcription_usd: number;
|
|
@@ -199,14 +252,19 @@ cost: {
|
|
|
199
252
|
Poller jusqu'à ce qu'un job atteigne `completed` ou `failed`. Respecte `pollInterval` et `timeout`.
|
|
200
253
|
|
|
201
254
|
**Réponse (`ReqVetReport`) :**
|
|
255
|
+
|
|
202
256
|
```ts
|
|
203
257
|
{
|
|
204
258
|
jobId: string;
|
|
205
|
-
html: string;
|
|
206
|
-
fields: ExtractedFields | null;
|
|
259
|
+
html: string; // HTML du compte rendu généré
|
|
260
|
+
fields: ExtractedFields | null; // null si aucun field_schema configuré
|
|
207
261
|
transcription: string;
|
|
208
262
|
animalName: string;
|
|
209
|
-
cost: {
|
|
263
|
+
cost: {
|
|
264
|
+
transcription_usd: number;
|
|
265
|
+
generation_usd: number;
|
|
266
|
+
total_usd: number;
|
|
267
|
+
}
|
|
210
268
|
metadata: Record<string, unknown>;
|
|
211
269
|
}
|
|
212
270
|
```
|
|
@@ -226,6 +284,7 @@ Régénérer le compte rendu d'un job terminé — par exemple avec des instruct
|
|
|
226
284
|
| `templateId` | `string` | Basculer vers un autre template |
|
|
227
285
|
|
|
228
286
|
**Réponse :**
|
|
287
|
+
|
|
229
288
|
```ts
|
|
230
289
|
{ job_id: string; status: 'completed'; result: { html: string; fields?: ExtractedFields } }
|
|
231
290
|
```
|
|
@@ -247,8 +306,14 @@ Ajouter un audio complémentaire à un job terminé. Le nouvel audio est transcr
|
|
|
247
306
|
| `templateId` | `string` | — | Basculer vers un autre template |
|
|
248
307
|
|
|
249
308
|
**Réponse :**
|
|
309
|
+
|
|
250
310
|
```ts
|
|
251
|
-
{
|
|
311
|
+
{
|
|
312
|
+
job_id: string;
|
|
313
|
+
status: 'amending';
|
|
314
|
+
amendment_number: number;
|
|
315
|
+
message: string;
|
|
316
|
+
}
|
|
252
317
|
```
|
|
253
318
|
|
|
254
319
|
Le job repasse à `completed` quand l'amendement est terminé. Utilisez `waitForJob()` ou écoutez l'événement webhook `job.amended`. Plusieurs amendements sont supportés — chacun est ajouté à la transcription complète.
|
|
@@ -275,6 +340,7 @@ Générer une version alternative d'un compte rendu terminé pour une audience s
|
|
|
275
340
|
| `custom` | Défini par `customInstructions` |
|
|
276
341
|
|
|
277
342
|
**Réponse (`ReqVetReformulation`) :**
|
|
343
|
+
|
|
278
344
|
```ts
|
|
279
345
|
{
|
|
280
346
|
id: string;
|
|
@@ -308,12 +374,12 @@ Générer une version alternative d'un compte rendu terminé pour une audience s
|
|
|
308
374
|
|
|
309
375
|
#### `createTemplate(params)` → `Template`
|
|
310
376
|
|
|
311
|
-
| Nom
|
|
312
|
-
|
|
313
|
-
| `name`
|
|
314
|
-
| `content`
|
|
315
|
-
| `description` | `string`
|
|
316
|
-
| `is_default`
|
|
377
|
+
| Nom | Type | Requis |
|
|
378
|
+
| ------------- | --------- | ------ |
|
|
379
|
+
| `name` | `string` | ✅ |
|
|
380
|
+
| `content` | `string` | ✅ |
|
|
381
|
+
| `description` | `string` | — |
|
|
382
|
+
| `is_default` | `boolean` | — |
|
|
317
383
|
|
|
318
384
|
#### `updateTemplate(templateId, updates)` → `Template`
|
|
319
385
|
|
|
@@ -462,10 +528,10 @@ import { verifyWebhookSignature } from '@reqvet-sdk/sdk/webhooks';
|
|
|
462
528
|
|
|
463
529
|
const { ok, reason } = verifyWebhookSignature({
|
|
464
530
|
secret: process.env.REQVET_WEBHOOK_SECRET!,
|
|
465
|
-
rawBody,
|
|
466
|
-
signature,
|
|
467
|
-
timestamp,
|
|
468
|
-
maxSkewMs: 5 * 60 * 1000,
|
|
531
|
+
rawBody, // corps brut de la requête — à lire AVANT JSON.parse
|
|
532
|
+
signature, // valeur de l'en-tête X-ReqVet-Signature
|
|
533
|
+
timestamp, // valeur de l'en-tête X-ReqVet-Timestamp
|
|
534
|
+
maxSkewMs: 5 * 60 * 1000, // rejeter les événements de plus de 5 min (défaut)
|
|
469
535
|
});
|
|
470
536
|
```
|
|
471
537
|
|
|
@@ -486,25 +552,245 @@ try {
|
|
|
486
552
|
const report = await reqvet.waitForJob(jobId);
|
|
487
553
|
} catch (err) {
|
|
488
554
|
if (err instanceof ReqVetError) {
|
|
489
|
-
console.error(err.message);
|
|
490
|
-
console.error(err.status);
|
|
491
|
-
console.error(err.body);
|
|
555
|
+
console.error(err.message); // message lisible par un humain
|
|
556
|
+
console.error(err.status); // statut HTTP (0 pour les erreurs réseau/timeout)
|
|
557
|
+
console.error(err.body); // corps brut de la réponse
|
|
492
558
|
}
|
|
493
559
|
}
|
|
494
560
|
```
|
|
495
561
|
|
|
496
|
-
| Statut | Signification
|
|
497
|
-
|
|
498
|
-
| `400`
|
|
499
|
-
| `401`
|
|
500
|
-
| `403`
|
|
501
|
-
| `404`
|
|
502
|
-
| `429`
|
|
503
|
-
| `500`
|
|
562
|
+
| Statut | Signification |
|
|
563
|
+
| ------ | ------------------------------------------------- |
|
|
564
|
+
| `400` | Erreur de validation — vérifiez `err.body.issues` |
|
|
565
|
+
| `401` | Clé API invalide ou manquante |
|
|
566
|
+
| `403` | Quota mensuel dépassé |
|
|
567
|
+
| `404` | Job ou template introuvable |
|
|
568
|
+
| `429` | Limite de débit dépassée — attendez et réessayez |
|
|
569
|
+
| `500` | Erreur interne ReqVet |
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## 9) API Partenaire / Revendeur
|
|
574
|
+
|
|
575
|
+
Ces méthodes sont réservées aux revendeurs (éditeurs logiciels) qui provisionnent et administrent des cliniques clientes. Elles nécessitent une clé API avec `role='reseller'`, distincte des clés d'organisation standard.
|
|
576
|
+
|
|
577
|
+
> **Isolation garantie** : un revendeur ne peut accéder qu'aux organisations qu'il a lui-même créées. La contrainte est appliquée en base de données (`parent_org_id = reseller.orgId`) — il n'est pas possible d'accéder aux données d'un autre revendeur, même avec un `orgId` connu.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
### `listOrganizations()`
|
|
582
|
+
|
|
583
|
+
Lister toutes les organisations (cliniques) provisionnées par le revendeur, enrichies de l'usage du mois courant.
|
|
584
|
+
|
|
585
|
+
**Réponse :**
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
{
|
|
589
|
+
organizations: Array<{
|
|
590
|
+
id: string;
|
|
591
|
+
name: string;
|
|
592
|
+
contact_email: string | null;
|
|
593
|
+
is_active: boolean;
|
|
594
|
+
monthly_quota: number | null;
|
|
595
|
+
external_id: string | null;
|
|
596
|
+
created_at: string;
|
|
597
|
+
usage: {
|
|
598
|
+
jobs_this_month: number;
|
|
599
|
+
quota_remaining: number | 'unlimited';
|
|
600
|
+
};
|
|
601
|
+
}>;
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Exemple :**
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
const reseller = new ReqVet(process.env.REQVET_RESELLER_KEY!);
|
|
609
|
+
|
|
610
|
+
const { organizations } = await reseller.listOrganizations();
|
|
611
|
+
|
|
612
|
+
for (const org of organizations) {
|
|
613
|
+
console.log(`${org.name} — ${org.usage.jobs_this_month} jobs ce mois / quota ${org.monthly_quota}`);
|
|
614
|
+
}
|
|
615
|
+
```
|
|
504
616
|
|
|
505
617
|
---
|
|
506
618
|
|
|
507
|
-
|
|
619
|
+
### `createOrganization(params)`
|
|
620
|
+
|
|
621
|
+
Provisionner une nouvelle organisation (clinique) sous le compte revendeur.
|
|
622
|
+
|
|
623
|
+
Cette méthode crée automatiquement :
|
|
624
|
+
- l'enregistrement de l'organisation dans ReqVet
|
|
625
|
+
- une clé API `rqv_live_...` pour la clinique (stockée hashée côté serveur)
|
|
626
|
+
- un secret de signature webhook `whsec_...`
|
|
627
|
+
|
|
628
|
+
**Paramètres :**
|
|
629
|
+
|
|
630
|
+
| Nom | Type | Requis | Description |
|
|
631
|
+
|-----|------|--------|-------------|
|
|
632
|
+
| `name` | `string` | ✅ | Nom de la clinique |
|
|
633
|
+
| `contactEmail` | `string` | — | Email de contact |
|
|
634
|
+
| `externalId` | `string` | — | Votre identifiant interne (active l'idempotence — voir ci-dessous) |
|
|
635
|
+
| `monthlyQuota` | `number` | — | Nombre max de jobs par mois (défaut : 100, max : 10 000) |
|
|
636
|
+
| `webhookUrl` | `string` | — | URL de webhook pour les événements de jobs de cette clinique |
|
|
637
|
+
|
|
638
|
+
**Idempotence via `externalId`**
|
|
639
|
+
|
|
640
|
+
Si `externalId` est fourni et qu'une organisation avec ce même identifiant existe déjà sous ce revendeur, la méthode retourne l'organisation existante sans créer de doublon. Le champ `message` est alors présent dans la réponse (`'Organization already exists (idempotent)'`), et `api_key` / `webhook_secret` sont absents (ils ne peuvent pas être récupérés après la création initiale).
|
|
641
|
+
|
|
642
|
+
**Réponse (statut 201 — première création) :**
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
{
|
|
646
|
+
organization: {
|
|
647
|
+
id: string;
|
|
648
|
+
name: string;
|
|
649
|
+
monthly_quota: number;
|
|
650
|
+
external_id: string | null;
|
|
651
|
+
};
|
|
652
|
+
api_key: string; // rqv_live_... — à stocker immédiatement, non récupérable ensuite
|
|
653
|
+
webhook_secret: string; // whsec_... — à stocker immédiatement, non récupérable ensuite
|
|
654
|
+
warning: string; // "Save api_key and webhook_secret now — they cannot be retrieved later!"
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Réponse (statut 200 — idempotent, organisation déjà existante) :**
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
{
|
|
662
|
+
message: 'Organization already exists (idempotent)';
|
|
663
|
+
organization: { id, name, monthly_quota, external_id, is_active };
|
|
664
|
+
// api_key et webhook_secret absents
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Exemple :**
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
const result = await reseller.createOrganization({
|
|
672
|
+
name: 'Clinique du Parc',
|
|
673
|
+
contactEmail: 'contact@clinique-du-parc.fr',
|
|
674
|
+
externalId: 'votre_id_interne_4892',
|
|
675
|
+
monthlyQuota: 500,
|
|
676
|
+
webhookUrl: 'https://votre-app.com/webhooks/reqvet',
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
if (!result.api_key) {
|
|
680
|
+
// Organisation déjà existante (idempotent) — récupérer la clé depuis votre propre stockage
|
|
681
|
+
const storedKey = await db.getApiKey(result.organization.id);
|
|
682
|
+
} else {
|
|
683
|
+
// Première création — stocker la clé et le secret immédiatement
|
|
684
|
+
await db.saveClinicCredentials({
|
|
685
|
+
orgId: result.organization.id,
|
|
686
|
+
apiKey: result.api_key,
|
|
687
|
+
webhookSecret: result.webhook_secret,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
### `getOrganization(orgId)`
|
|
695
|
+
|
|
696
|
+
Obtenir les détails et l'usage du mois courant d'une organisation spécifique.
|
|
697
|
+
|
|
698
|
+
**Paramètres :**
|
|
699
|
+
|
|
700
|
+
| Nom | Type | Requis | Description |
|
|
701
|
+
|-----|------|--------|-------------|
|
|
702
|
+
| `orgId` | `string` | ✅ | UUID de l'organisation |
|
|
703
|
+
|
|
704
|
+
**Réponse :** `PartnerOrganization` (même structure que les éléments de `listOrganizations`, avec `usage`)
|
|
705
|
+
|
|
706
|
+
**Exemple :**
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
const org = await reseller.getOrganization('uuid-de-la-clinique');
|
|
710
|
+
console.log(`Quota restant : ${org.usage.quota_remaining}`);
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
### `updateOrganization(orgId, updates)`
|
|
716
|
+
|
|
717
|
+
Modifier le quota mensuel, l'état d'activation, ou l'URL de webhook d'une organisation.
|
|
718
|
+
|
|
719
|
+
Tous les champs sont optionnels — seuls les champs fournis sont mis à jour.
|
|
720
|
+
|
|
721
|
+
**Paramètres :**
|
|
722
|
+
|
|
723
|
+
| Nom | Type | Description |
|
|
724
|
+
|-----|------|-------------|
|
|
725
|
+
| `orgId` | `string` | UUID de l'organisation |
|
|
726
|
+
| `updates.monthlyQuota` | `number` | Nouveau quota mensuel (1–10 000) |
|
|
727
|
+
| `updates.isActive` | `boolean` | `false` pour suspendre la clinique (révoque aussi ses clés API) |
|
|
728
|
+
| `updates.webhookUrl` | `string` | Nouvelle URL de webhook (`null` pour supprimer) |
|
|
729
|
+
|
|
730
|
+
> **Suspension** : passer `isActive: false` désactive l'organisation **et** révoque toutes ses clés API en cascade. Les jobs déjà en cours ne sont pas interrompus. Pour réactiver, passer `isActive: true` — les clés API restent révoquées et doivent être régénérées manuellement si nécessaire.
|
|
731
|
+
|
|
732
|
+
**Réponse :**
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
{
|
|
736
|
+
id: string;
|
|
737
|
+
name: string;
|
|
738
|
+
is_active: boolean;
|
|
739
|
+
monthly_quota: number;
|
|
740
|
+
external_id: string | null;
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**Exemples :**
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
// Augmenter le quota
|
|
748
|
+
await reseller.updateOrganization(orgId, { monthlyQuota: 1000 });
|
|
749
|
+
|
|
750
|
+
// Suspendre une clinique (non-paiement, etc.)
|
|
751
|
+
await reseller.updateOrganization(orgId, { isActive: false });
|
|
752
|
+
|
|
753
|
+
// Réactiver
|
|
754
|
+
await reseller.updateOrganization(orgId, { isActive: true });
|
|
755
|
+
|
|
756
|
+
// Mettre à jour le webhook
|
|
757
|
+
await reseller.updateOrganization(orgId, {
|
|
758
|
+
webhookUrl: 'https://votre-app.com/webhooks/reqvet/v2',
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
### `deactivateOrganization(orgId)`
|
|
765
|
+
|
|
766
|
+
Désactiver définitivement une organisation et révoquer toutes ses clés API.
|
|
767
|
+
|
|
768
|
+
Il s'agit d'un **soft delete** : les données (jobs, transcriptions, comptes rendus) sont conservées en base pour respecter les obligations RGPD et permettre un audit trail. L'organisation ne peut plus créer de nouveaux jobs.
|
|
769
|
+
|
|
770
|
+
**Paramètres :**
|
|
771
|
+
|
|
772
|
+
| Nom | Type | Requis | Description |
|
|
773
|
+
|-----|------|--------|-------------|
|
|
774
|
+
| `orgId` | `string` | ✅ | UUID de l'organisation |
|
|
775
|
+
|
|
776
|
+
**Réponse :**
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
{ success: true; message: 'Organization and API keys deactivated' }
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**Exemple :**
|
|
783
|
+
|
|
784
|
+
```ts
|
|
785
|
+
await reseller.deactivateOrganization(orgId);
|
|
786
|
+
// L'organisation est désormais inactive, ses clés API sont révoquées
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## 10) Checklist d'intégration
|
|
792
|
+
|
|
793
|
+
**Intégration standard (clinique)**
|
|
508
794
|
|
|
509
795
|
- [ ] SDK utilisé **côté serveur uniquement** — clé API jamais dans les bundles navigateur
|
|
510
796
|
- [ ] `listTemplates()` appelé au démarrage pour découvrir les `templateId` disponibles
|
|
@@ -514,3 +800,11 @@ try {
|
|
|
514
800
|
- [ ] Vérification anti-replay du timestamp activée (`maxSkewMs`)
|
|
515
801
|
- [ ] Idempotence implémentée — dédoublonnage sur `job_id + event`
|
|
516
802
|
- [ ] `REQVET_API_KEY` et `REQVET_WEBHOOK_SECRET` stockés dans des variables d'environnement, jamais en dur dans le code
|
|
803
|
+
|
|
804
|
+
**Intégration revendeur (multi-tenant)**
|
|
805
|
+
|
|
806
|
+
- [ ] 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
|
|
809
|
+
- [ ] Suspension de clinique gérée via `updateOrganization(orgId, { isActive: false })` (et non `deactivateOrganization` qui est irréversible)
|
|
810
|
+
- [ ] Usage mensuel (`usage.jobs_this_month`, `usage.quota_remaining`) consulté via `listOrganizations()` ou `getOrganization()` pour le monitoring et la facturation
|
package/SECURITY.md
CHANGED
|
@@ -29,7 +29,7 @@ Example proxy route (Next.js App Router):
|
|
|
29
29
|
```ts
|
|
30
30
|
// app/api/reqvet/generate/route.ts
|
|
31
31
|
import { NextRequest, NextResponse } from 'next/server';
|
|
32
|
-
import ReqVet from '@reqvet/sdk';
|
|
32
|
+
import ReqVet from '@reqvet-sdk/sdk';
|
|
33
33
|
|
|
34
34
|
const reqvet = new ReqVet(process.env.REQVET_API_KEY!);
|
|
35
35
|
|
|
@@ -37,7 +37,21 @@ export async function POST(req: NextRequest) {
|
|
|
37
37
|
const form = await req.formData();
|
|
38
38
|
const audio = form.get('audio') as File;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Use getSignedUploadUrl() instead of uploadAudio() for server-side proxies.
|
|
41
|
+
// uploadAudio() posts to /api/v1/upload (Vercel Serverless Function, ~4.5 MB limit).
|
|
42
|
+
// getSignedUploadUrl() uploads directly to Supabase — no size limit.
|
|
43
|
+
const { uploadUrl, path } = await reqvet.getSignedUploadUrl(
|
|
44
|
+
audio.name || 'consultation.webm',
|
|
45
|
+
audio.type || 'audio/webm',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const audioBuffer = Buffer.from(await audio.arrayBuffer());
|
|
49
|
+
await fetch(uploadUrl, {
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
headers: { 'Content-Type': audio.type || 'audio/webm' },
|
|
52
|
+
body: audioBuffer,
|
|
53
|
+
});
|
|
54
|
+
|
|
41
55
|
const job = await reqvet.createJob({
|
|
42
56
|
audioFile: path,
|
|
43
57
|
animalName: form.get('animalName') as string,
|
|
@@ -103,6 +117,46 @@ await cache.set(key, true, { ttl: 86400 });
|
|
|
103
117
|
// process...
|
|
104
118
|
```
|
|
105
119
|
|
|
120
|
+
## Clés revendeur et credentials cliniques
|
|
121
|
+
|
|
122
|
+
Les revendeurs manipulent deux types de secrets supplémentaires qui exigent une attention particulière.
|
|
123
|
+
|
|
124
|
+
### Clé API revendeur
|
|
125
|
+
|
|
126
|
+
La clé revendeur (`rqv_live_...` avec `role='reseller'`) donne accès à l'ensemble des cliniques que vous administrez. Elle est plus sensible qu'une clé clinique standard.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Variable d'environnement dédiée, distincte de la clé clinique
|
|
130
|
+
REQVET_RESELLER_KEY=rqv_live_...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Ne jamais** utiliser la clé revendeur côté client ni la partager avec les cliniques.
|
|
134
|
+
|
|
135
|
+
### `api_key` et `webhook_secret` retournés par `createOrganization`
|
|
136
|
+
|
|
137
|
+
Ces deux valeurs sont **affichées une seule fois** au moment de la création. Après la réponse HTTP initiale, elles ne peuvent pas être récupérées (seul le hash SHA-256 de la clé est stocké côté serveur).
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const result = await reseller.createOrganization({ name: 'Clinique du Parc', externalId: 'id_4892' });
|
|
141
|
+
|
|
142
|
+
// ✅ Stocker immédiatement dans votre vault ou base chiffrée
|
|
143
|
+
await secretsVault.store({
|
|
144
|
+
orgId: result.organization.id,
|
|
145
|
+
apiKey: result.api_key, // non récupérable après cette réponse
|
|
146
|
+
webhookSecret: result.webhook_secret, // non récupérable après cette réponse
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ✅ Transmettre la clé à la clinique de manière sécurisée (canal chiffré, jamais par email)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Si une clé clinique est perdue ou compromise, le seul recours est de désactiver l'organisation via `deactivateOrganization()` et d'en créer une nouvelle.
|
|
153
|
+
|
|
154
|
+
### Isolation entre revendeurs
|
|
155
|
+
|
|
156
|
+
L'API Partner filtre toutes les requêtes sur `parent_org_id = reseller.orgId` côté base de données. Il n'est pas possible d'accéder aux cliniques d'un autre revendeur, même en connaissant un `orgId` valide. Ne pas tenter de contourner cette isolation.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
106
160
|
## API key rotation
|
|
107
161
|
|
|
108
162
|
If a key is compromised:
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface UploadResult {
|
|
|
15
15
|
content_type: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface SignedUploadResult {
|
|
19
|
+
/** Presigned URL for direct PUT upload to Supabase storage */
|
|
20
|
+
uploadUrl: string;
|
|
21
|
+
/** Storage path to pass to createJob({ audioFile: path }) */
|
|
22
|
+
path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export interface JobResult {
|
|
19
26
|
job_id: string;
|
|
20
27
|
status: 'pending' | 'transcribing' | 'generating' | 'completed' | 'failed' | 'amending';
|
|
@@ -181,6 +188,52 @@ export interface ReqVetReformulation {
|
|
|
181
188
|
created_at: string;
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
// ─── Partner / Reseller Types ────────────────────────────────
|
|
192
|
+
|
|
193
|
+
export interface OrganizationUsage {
|
|
194
|
+
jobs_this_month: number;
|
|
195
|
+
quota_remaining: number | 'unlimited';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface PartnerOrganization {
|
|
199
|
+
id: string;
|
|
200
|
+
name: string;
|
|
201
|
+
contact_email: string | null;
|
|
202
|
+
is_active: boolean;
|
|
203
|
+
monthly_quota: number | null;
|
|
204
|
+
external_id: string | null;
|
|
205
|
+
created_at: string;
|
|
206
|
+
usage?: OrganizationUsage;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CreateOrganizationParams {
|
|
210
|
+
name: string;
|
|
211
|
+
contactEmail?: string;
|
|
212
|
+
/** Your internal ID — enables idempotency (same externalId returns the existing org) */
|
|
213
|
+
externalId?: string;
|
|
214
|
+
/** Max jobs per month (default: 100, max: 10 000) */
|
|
215
|
+
monthlyQuota?: number;
|
|
216
|
+
webhookUrl?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface UpdateOrganizationParams {
|
|
220
|
+
monthlyQuota?: number;
|
|
221
|
+
/** Set to false to suspend the clinic and revoke its API keys */
|
|
222
|
+
isActive?: boolean;
|
|
223
|
+
webhookUrl?: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface CreateOrganizationResult {
|
|
227
|
+
organization: Pick<PartnerOrganization, 'id' | 'name' | 'monthly_quota' | 'external_id'>;
|
|
228
|
+
/** The clinic's API key — returned only once, store it securely! */
|
|
229
|
+
api_key: string;
|
|
230
|
+
/** Webhook signing secret — returned only once, store it securely! */
|
|
231
|
+
webhook_secret: string;
|
|
232
|
+
warning: string;
|
|
233
|
+
/** Returned instead of api_key/webhook_secret when the org already exists (idempotent) */
|
|
234
|
+
message?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
184
237
|
// ─── Client ──────────────────────────────────────────────────
|
|
185
238
|
|
|
186
239
|
export declare class ReqVetError extends Error {
|
|
@@ -201,7 +254,18 @@ export declare class ReqVet {
|
|
|
201
254
|
generateReport(params: GenerateReportParams & { waitForResult?: false }): Promise<JobResult>;
|
|
202
255
|
generateReport(params: GenerateReportParams): Promise<JobResult | ReqVetReport>;
|
|
203
256
|
|
|
204
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Get a presigned URL for direct upload to Supabase storage.
|
|
259
|
+
* Recommended for server-side proxies — bypasses Vercel's ~4.5 MB payload limit.
|
|
260
|
+
* PUT the audio buffer to uploadUrl, then pass path to createJob().
|
|
261
|
+
*/
|
|
262
|
+
getSignedUploadUrl(fileName: string, contentType: string): Promise<SignedUploadResult>;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Upload an audio file via ReqVet's /api/v1/upload endpoint.
|
|
266
|
+
* ⚠️ Subject to Vercel Serverless Function payload limit (~4.5 MB).
|
|
267
|
+
* For files > 4 MB in server-side contexts, use getSignedUploadUrl() instead.
|
|
268
|
+
*/
|
|
205
269
|
uploadAudio(audio: Blob | Buffer | File, fileName?: string): Promise<UploadResult>;
|
|
206
270
|
|
|
207
271
|
/** Create a generation job */
|
|
@@ -254,6 +318,30 @@ export declare class ReqVet {
|
|
|
254
318
|
/** Delete a template */
|
|
255
319
|
deleteTemplate(templateId: string): Promise<{ success: boolean }>;
|
|
256
320
|
|
|
321
|
+
// ─── Partner / Reseller (requires role='reseller' API key) ──
|
|
322
|
+
|
|
323
|
+
/** List all organizations provisioned by the reseller */
|
|
324
|
+
listOrganizations(): Promise<{ organizations: PartnerOrganization[] }>;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Provision a new organization/clinic.
|
|
328
|
+
* Idempotent via externalId — returns existing org if already provisioned.
|
|
329
|
+
* ⚠️ api_key and webhook_secret are returned only once.
|
|
330
|
+
*/
|
|
331
|
+
createOrganization(params: CreateOrganizationParams): Promise<CreateOrganizationResult>;
|
|
332
|
+
|
|
333
|
+
/** Get details and current month usage of a specific organization */
|
|
334
|
+
getOrganization(orgId: string): Promise<PartnerOrganization>;
|
|
335
|
+
|
|
336
|
+
/** Update an organization's quota, status, or webhook URL */
|
|
337
|
+
updateOrganization(
|
|
338
|
+
orgId: string,
|
|
339
|
+
updates: UpdateOrganizationParams,
|
|
340
|
+
): Promise<Pick<PartnerOrganization, 'id' | 'name' | 'is_active' | 'monthly_quota' | 'external_id'>>;
|
|
341
|
+
|
|
342
|
+
/** Deactivate an organization and revoke all its API keys (soft delete) */
|
|
343
|
+
deactivateOrganization(orgId: string): Promise<{ success: boolean; message: string }>;
|
|
344
|
+
|
|
257
345
|
/** Health check */
|
|
258
346
|
health(): Promise<{ status: string; services: Record<string, string> }>;
|
|
259
347
|
}
|
package/src/index.js
CHANGED
|
@@ -108,9 +108,37 @@ class ReqVet {
|
|
|
108
108
|
|
|
109
109
|
// ─── Upload ────────────────────────────────────────────────
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Get a presigned upload URL for direct upload to ReqVet storage (Supabase).
|
|
113
|
+
*
|
|
114
|
+
* Recommended for server-side integrations (e.g. Next.js proxy routes).
|
|
115
|
+
* The file goes directly to Supabase — it never passes through a Vercel
|
|
116
|
+
* Serverless Function, so there is no ~4.5 MB payload limit.
|
|
117
|
+
*
|
|
118
|
+
* Flow:
|
|
119
|
+
* 1. getSignedUploadUrl(fileName, contentType) — tiny JSON request, no file.
|
|
120
|
+
* 2. PUT the audio buffer to uploadUrl (direct to Supabase).
|
|
121
|
+
* 3. Pass path to createJob({ audioFile: path }).
|
|
122
|
+
*
|
|
123
|
+
* @param {string} fileName - File name (e.g. 'consultation.webm')
|
|
124
|
+
* @param {string} contentType - MIME type (e.g. 'audio/webm')
|
|
125
|
+
* @returns {Promise<{uploadUrl: string, path: string}>}
|
|
126
|
+
*/
|
|
127
|
+
async getSignedUploadUrl(fileName, contentType) {
|
|
128
|
+
return this._fetch('POST', '/api/v1/storage/signed-upload', {
|
|
129
|
+
file_name: fileName,
|
|
130
|
+
content_type: contentType,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
111
134
|
/**
|
|
112
135
|
* Upload an audio file to ReqVet storage.
|
|
113
136
|
*
|
|
137
|
+
* ⚠️ This method POSTs the file to /api/v1/upload, which runs as a
|
|
138
|
+
* Vercel Serverless Function (~4.5 MB request limit). For server-side
|
|
139
|
+
* proxies (Next.js, Express…) handling files > 4 MB, prefer
|
|
140
|
+
* getSignedUploadUrl() + a direct PUT to avoid this limit.
|
|
141
|
+
*
|
|
114
142
|
* @param {Blob|Buffer|File} audio - The audio file
|
|
115
143
|
* @param {string} [fileName] - File name
|
|
116
144
|
* @returns {Promise<{audio_file: string, size_bytes: number}>}
|
|
@@ -388,6 +416,87 @@ class ReqVet {
|
|
|
388
416
|
return this._fetch('DELETE', `/api/v1/templates/${templateId}`);
|
|
389
417
|
}
|
|
390
418
|
|
|
419
|
+
// ─── Partner / Reseller ────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* List all organizations provisioned by the reseller.
|
|
423
|
+
* Requires a reseller API key (role='reseller').
|
|
424
|
+
*
|
|
425
|
+
* @returns {Promise<{organizations: Object[]}>}
|
|
426
|
+
*/
|
|
427
|
+
async listOrganizations() {
|
|
428
|
+
return this._fetch('GET', '/api/v1/partner/orgs');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Provision a new organization (clinic) under the reseller account.
|
|
433
|
+
* Requires a reseller API key (role='reseller').
|
|
434
|
+
*
|
|
435
|
+
* Idempotent via externalId: if an org with the same externalId already
|
|
436
|
+
* exists under this reseller, the existing one is returned (no duplicate).
|
|
437
|
+
*
|
|
438
|
+
* ⚠️ The returned api_key and webhook_secret are shown only once — store them securely.
|
|
439
|
+
*
|
|
440
|
+
* @param {Object} params
|
|
441
|
+
* @param {string} params.name - Clinic name
|
|
442
|
+
* @param {string} [params.contactEmail]
|
|
443
|
+
* @param {string} [params.externalId] - Your internal ID (enables idempotency)
|
|
444
|
+
* @param {number} [params.monthlyQuota] - Job quota per month (default: 100, max: 10 000)
|
|
445
|
+
* @param {string} [params.webhookUrl] - Webhook URL for job results
|
|
446
|
+
* @returns {Promise<Object>}
|
|
447
|
+
*/
|
|
448
|
+
async createOrganization({ name, contactEmail, externalId, monthlyQuota, webhookUrl }) {
|
|
449
|
+
return this._fetch('POST', '/api/v1/partner/orgs', {
|
|
450
|
+
name,
|
|
451
|
+
contact_email: contactEmail,
|
|
452
|
+
external_id: externalId,
|
|
453
|
+
monthly_quota: monthlyQuota,
|
|
454
|
+
webhook_url: webhookUrl,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get details and current month usage of a specific organization.
|
|
460
|
+
* Requires a reseller API key (role='reseller').
|
|
461
|
+
*
|
|
462
|
+
* @param {string} orgId
|
|
463
|
+
* @returns {Promise<Object>}
|
|
464
|
+
*/
|
|
465
|
+
async getOrganization(orgId) {
|
|
466
|
+
return this._fetch('GET', `/api/v1/partner/orgs/${orgId}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Update an organization's quota, status, or webhook URL.
|
|
471
|
+
* Requires a reseller API key (role='reseller').
|
|
472
|
+
*
|
|
473
|
+
* @param {string} orgId
|
|
474
|
+
* @param {Object} updates
|
|
475
|
+
* @param {number} [updates.monthlyQuota]
|
|
476
|
+
* @param {boolean} [updates.isActive] - Set to false to suspend the clinic
|
|
477
|
+
* @param {string} [updates.webhookUrl]
|
|
478
|
+
* @returns {Promise<Object>}
|
|
479
|
+
*/
|
|
480
|
+
async updateOrganization(orgId, { monthlyQuota, isActive, webhookUrl } = {}) {
|
|
481
|
+
return this._fetch('PATCH', `/api/v1/partner/orgs/${orgId}`, {
|
|
482
|
+
monthly_quota: monthlyQuota,
|
|
483
|
+
is_active: isActive,
|
|
484
|
+
webhook_url: webhookUrl,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Deactivate an organization and revoke all its API keys.
|
|
490
|
+
* Soft delete — data is preserved for audit/GDPR purposes.
|
|
491
|
+
* Requires a reseller API key (role='reseller').
|
|
492
|
+
*
|
|
493
|
+
* @param {string} orgId
|
|
494
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
495
|
+
*/
|
|
496
|
+
async deactivateOrganization(orgId) {
|
|
497
|
+
return this._fetch('DELETE', `/api/v1/partner/orgs/${orgId}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
391
500
|
// ─── Health ────────────────────────────────────────────────
|
|
392
501
|
|
|
393
502
|
/**
|