@voxnova/sdk 1.0.0 → 1.1.0
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 +26 -2
- package/dist/client.d.ts +319 -1
- package/dist/client.js +88 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/webhook.d.ts +23 -0
- package/dist/webhook.js +8 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
SDK TypeScript de la plateforme développeurs VoxNova (Node 18+, sans dépendance).
|
|
4
4
|
|
|
5
|
-
- `VoxNova` : client de l'API REST
|
|
5
|
+
- `VoxNova` : client de l'API REST (clé d'entreprise `vxk_…` ou clé revendeur `vxr_…`) : **SMS** (envoi, suivi de remise, crédits), webhooks, applications, endpoints SIP, numéros, appels, enregistrements, journal, **scénarios d'appel** (`flows`), utilisateurs, postes, files d'attente, groupes d'appel, serveurs vocaux, accès brut au panneau (`panel`) et **espace revendeur** (`reseller.*`, `forOrganization()`).
|
|
6
6
|
- `VoiceResponse` : construction des instructions renvoyées par votre webhook (dial plan en code).
|
|
7
|
-
- `verifySignature` : vérification
|
|
7
|
+
- `verifySignature` : vérification des rappels des applications vocales (`t=…,v1=…`).
|
|
8
|
+
- `verifyWebhook` : vérification des webhooks d'entreprise et de revendeur (`sha256=…` : `sms.status`, `call.completed`).
|
|
8
9
|
|
|
9
10
|
```ts
|
|
10
11
|
import { VoxNova, VoiceResponse, verifySignature } from '@voxnova/sdk';
|
|
@@ -17,4 +18,27 @@ const r = new VoiceResponse().say(42).gather({ numDigits: 1, action: 'https://ex
|
|
|
17
18
|
res.json(r);
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
```ts
|
|
22
|
+
// SMS et suivi
|
|
23
|
+
const sms = await vx.sms.send({ to: '0612345678', text: 'Votre code : 1234', tag: 'otp', sender: 'hCloud' });
|
|
24
|
+
const state = await vx.sms.get(sms.id); // status : queued → sent → delivered ; events[]
|
|
25
|
+
const { balance, monthlyLeft } = await vx.sms.balance();
|
|
26
|
+
|
|
27
|
+
// Revendeur : agir au nom d'un client (clé vxr_…)
|
|
28
|
+
const reseller = new VoxNova({ apiKey: process.env.VXR!, baseUrl: 'https://api.exemple.fr' });
|
|
29
|
+
const client = reseller.forOrganization('<id du client>');
|
|
30
|
+
await client.extensions.create({ number: '1001', displayName: 'Accueil' });
|
|
31
|
+
await client.flows.create({ name: 'Standard', graph: { nodes: [/* … */], edges: [/* … */] } });
|
|
32
|
+
await reseller.reseller.clients.applySmsPack('<id du client>', '<id du pack>');
|
|
33
|
+
|
|
34
|
+
// Webhooks (Express) : vérifier la signature sur le corps brut
|
|
35
|
+
app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {
|
|
36
|
+
if (!verifyWebhook(process.env.WH_SECRET!, req.body, req.header('x-voxnova-signature'))) return res.sendStatus(401);
|
|
37
|
+
const event = JSON.parse(req.body.toString()); // { event: 'sms.status', data: { status: 'delivered', … } }
|
|
38
|
+
res.sendStatus(200);
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Documentation interactive : `/api/v1/docs` (Swagger) ; guide : chapitres 19 et 30.
|
|
43
|
+
|
|
20
44
|
Documentation complète : chapitre 19 « Plateforme développeurs » et `GET /api/v1/openapi.json`.
|
package/dist/client.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export declare class VoxNovaError extends Error {
|
|
|
7
7
|
constructor(message: string, status: number, body: unknown);
|
|
8
8
|
}
|
|
9
9
|
export interface ClientOptions {
|
|
10
|
+
/** Clé d'entreprise vxk_… ou clé revendeur vxr_… */
|
|
10
11
|
apiKey: string;
|
|
12
|
+
/** Clé revendeur uniquement : identifiant (ou identifiant court) du client au nom duquel agir (en-tête X-Organization). */
|
|
13
|
+
organization?: string;
|
|
11
14
|
/** Adresse du panneau, ex. https://voip.example.com */
|
|
12
15
|
baseUrl: string;
|
|
13
16
|
/** Délai par requête (15 s par défaut). */
|
|
@@ -69,8 +72,24 @@ export interface Endpoint {
|
|
|
69
72
|
createdAt: string;
|
|
70
73
|
registered?: boolean;
|
|
71
74
|
sip?: SipCredentials;
|
|
75
|
+
/** Configuration prête pour @voxnova/webphone (softphone navigateur) ; null si la passerelle WebRTC n'est pas activée. */
|
|
76
|
+
webphone?: WebphoneConfig | null;
|
|
72
77
|
registrations?: Registration[];
|
|
73
78
|
}
|
|
79
|
+
export interface WebphoneConfig {
|
|
80
|
+
wsServers: string[];
|
|
81
|
+
uri: string;
|
|
82
|
+
authorizationUser: string;
|
|
83
|
+
password: string;
|
|
84
|
+
realm: string;
|
|
85
|
+
displayName?: string;
|
|
86
|
+
registerExpires?: number;
|
|
87
|
+
iceServers?: {
|
|
88
|
+
urls: string | string[];
|
|
89
|
+
username?: string;
|
|
90
|
+
credential?: string;
|
|
91
|
+
}[];
|
|
92
|
+
}
|
|
74
93
|
export interface EndpointInput {
|
|
75
94
|
number?: string;
|
|
76
95
|
name: string;
|
|
@@ -104,12 +123,122 @@ export interface Page<T> {
|
|
|
104
123
|
total: number;
|
|
105
124
|
page: number;
|
|
106
125
|
}
|
|
126
|
+
export type Json = Record<string, unknown>;
|
|
127
|
+
export interface Sms {
|
|
128
|
+
id: string;
|
|
129
|
+
to: string;
|
|
130
|
+
text: string;
|
|
131
|
+
status: 'queued' | 'sent' | 'delivered' | 'failed';
|
|
132
|
+
providerStatus: string | null;
|
|
133
|
+
messageId: string | null;
|
|
134
|
+
sender: string | null;
|
|
135
|
+
tag: string | null;
|
|
136
|
+
type: 'transactional' | 'marketing' | null;
|
|
137
|
+
segments: number;
|
|
138
|
+
channel: string | null;
|
|
139
|
+
error: string | null;
|
|
140
|
+
createdAt: string;
|
|
141
|
+
sentAt: string | null;
|
|
142
|
+
deliveredAt: string | null;
|
|
143
|
+
events?: {
|
|
144
|
+
status: string;
|
|
145
|
+
at: string;
|
|
146
|
+
}[];
|
|
147
|
+
}
|
|
148
|
+
export interface SmsInput {
|
|
149
|
+
to: string;
|
|
150
|
+
text: string;
|
|
151
|
+
sender?: string;
|
|
152
|
+
tag?: string;
|
|
153
|
+
type?: 'transactional' | 'marketing';
|
|
154
|
+
channel?: 'sms' | 'whatsapp' | 'auto';
|
|
155
|
+
}
|
|
156
|
+
export interface SmsCredits {
|
|
157
|
+
monthlyLimit: number;
|
|
158
|
+
monthlyUsed: number;
|
|
159
|
+
monthlyLeft: number;
|
|
160
|
+
balance: number;
|
|
161
|
+
available: number;
|
|
162
|
+
}
|
|
163
|
+
export interface SmsPack {
|
|
164
|
+
id: string;
|
|
165
|
+
name: string;
|
|
166
|
+
kind: 'MONTHLY' | 'LIFETIME';
|
|
167
|
+
credits: number;
|
|
168
|
+
priceCents: number;
|
|
169
|
+
active: boolean;
|
|
170
|
+
}
|
|
171
|
+
export interface SmsPackInput {
|
|
172
|
+
name: string;
|
|
173
|
+
kind: 'MONTHLY' | 'LIFETIME';
|
|
174
|
+
credits: number;
|
|
175
|
+
priceCents?: number;
|
|
176
|
+
active?: boolean;
|
|
177
|
+
}
|
|
178
|
+
export interface SenderBlock {
|
|
179
|
+
id: string;
|
|
180
|
+
kind: 'exact' | 'prefix' | 'contains';
|
|
181
|
+
value: string;
|
|
182
|
+
reason: string;
|
|
183
|
+
}
|
|
184
|
+
export interface SenderBlockInput {
|
|
185
|
+
value: string;
|
|
186
|
+
kind?: SenderBlock['kind'];
|
|
187
|
+
reason?: string;
|
|
188
|
+
}
|
|
189
|
+
export type WebhookEventName = 'call.completed' | 'sms.status';
|
|
190
|
+
export interface Webhook {
|
|
191
|
+
id: string;
|
|
192
|
+
url: string;
|
|
193
|
+
events: WebhookEventName[];
|
|
194
|
+
enabled: boolean;
|
|
195
|
+
lastStatus: string | null;
|
|
196
|
+
createdAt: string;
|
|
197
|
+
secret?: string;
|
|
198
|
+
}
|
|
199
|
+
export interface WebhookInput {
|
|
200
|
+
url: string;
|
|
201
|
+
events?: WebhookEventName[];
|
|
202
|
+
enabled?: boolean;
|
|
203
|
+
}
|
|
204
|
+
export interface Flow {
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
number: string | null;
|
|
208
|
+
description: string;
|
|
209
|
+
enabled: boolean;
|
|
210
|
+
graph: {
|
|
211
|
+
nodes: Json[];
|
|
212
|
+
edges: Json[];
|
|
213
|
+
};
|
|
214
|
+
updatedAt?: string;
|
|
215
|
+
}
|
|
216
|
+
export interface FlowInput {
|
|
217
|
+
name: string;
|
|
218
|
+
number?: string | null;
|
|
219
|
+
description?: string;
|
|
220
|
+
enabled?: boolean;
|
|
221
|
+
graph: {
|
|
222
|
+
nodes: Json[];
|
|
223
|
+
edges: Json[];
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export interface ClientInput {
|
|
227
|
+
company: string;
|
|
228
|
+
name: string;
|
|
229
|
+
email: string;
|
|
230
|
+
password: string;
|
|
231
|
+
product?: string;
|
|
232
|
+
trialDays?: number;
|
|
233
|
+
}
|
|
107
234
|
export declare class VoxNova {
|
|
108
235
|
private readonly opts;
|
|
109
236
|
private readonly base;
|
|
110
237
|
private readonly f;
|
|
111
238
|
constructor(opts: ClientOptions);
|
|
112
|
-
/**
|
|
239
|
+
/** Revendeur : client agissant au nom d'un de vos clients (même clé vxr_, en-tête X-Organization). */
|
|
240
|
+
forOrganization(organization: string): VoxNova;
|
|
241
|
+
/** Requête brute vers l'API (chemin commençant par /api/). */
|
|
113
242
|
request<T>(method: string, path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
114
243
|
readonly account: {
|
|
115
244
|
get: () => Promise<{
|
|
@@ -266,4 +395,193 @@ export declare class VoxNova {
|
|
|
266
395
|
errors?: boolean;
|
|
267
396
|
}) => Promise<Page<Record<string, unknown>>>;
|
|
268
397
|
};
|
|
398
|
+
readonly sms: {
|
|
399
|
+
/** Met un SMS en file (statuts : webhook sms.status ou `get`). Refusé sans crédit ou si l'expéditeur est interdit. */
|
|
400
|
+
send: (input: SmsInput) => Promise<Sms>;
|
|
401
|
+
list: (opts?: {
|
|
402
|
+
page?: number;
|
|
403
|
+
pageSize?: number;
|
|
404
|
+
status?: string;
|
|
405
|
+
to?: string;
|
|
406
|
+
tag?: string;
|
|
407
|
+
since?: string;
|
|
408
|
+
}) => Promise<Page<Sms>>;
|
|
409
|
+
/** Par identifiant ou messageId du fournisseur, avec les événements de remise (accepted, sent, delivered…). */
|
|
410
|
+
get: (id: string) => Promise<Sms>;
|
|
411
|
+
/** Forfait du mois (remis à zéro chaque mois) et solde à vie. */
|
|
412
|
+
balance: () => Promise<SmsCredits>;
|
|
413
|
+
};
|
|
414
|
+
/** Webhooks de l'entreprise : call.completed, sms.status (signés, voir `verifyWebhook`). */
|
|
415
|
+
readonly webhooks: {
|
|
416
|
+
list: () => Promise<Webhook[]>;
|
|
417
|
+
/** Le secret de signature n'est renvoyé qu'à la création. */
|
|
418
|
+
create: (input: WebhookInput) => Promise<Webhook>;
|
|
419
|
+
update: (id: string, input: Partial<WebhookInput>) => Promise<Webhook>;
|
|
420
|
+
delete: (id: string) => Promise<{
|
|
421
|
+
ok: true;
|
|
422
|
+
}>;
|
|
423
|
+
};
|
|
424
|
+
/** Toute route du panneau (voir la documentation Swagger) : `vx.panel.get('/api/centrex/queues')`. */
|
|
425
|
+
readonly panel: {
|
|
426
|
+
get: <T = unknown>(path: string, query?: Record<string, string | number | boolean | undefined>) => Promise<T>;
|
|
427
|
+
post: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
428
|
+
put: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
429
|
+
patch: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
430
|
+
delete: <T = unknown>(path: string) => Promise<T>;
|
|
431
|
+
};
|
|
432
|
+
private crud;
|
|
433
|
+
/** Utilisateurs de l'entreprise (invitation par e-mail, droits). */
|
|
434
|
+
readonly users: {
|
|
435
|
+
invite: (id: string) => Promise<Json>;
|
|
436
|
+
permissions: (id: string, permissions: string[]) => Promise<Json>;
|
|
437
|
+
list: () => Promise<Json[]>;
|
|
438
|
+
create: (input: Json) => Promise<Json>;
|
|
439
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
440
|
+
delete: (id: string) => Promise<{
|
|
441
|
+
ok: true;
|
|
442
|
+
}>;
|
|
443
|
+
};
|
|
444
|
+
/** Postes (extensions) : création, modification complète, mot de passe SIP, état d'enregistrement. */
|
|
445
|
+
readonly extensions: {
|
|
446
|
+
sipAccount: (id: string) => Promise<Json>;
|
|
447
|
+
resetSipPassword: (id: string) => Promise<Json>;
|
|
448
|
+
status: (id: string) => Promise<Json>;
|
|
449
|
+
list: () => Promise<Json[]>;
|
|
450
|
+
create: (input: Json) => Promise<Json>;
|
|
451
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
452
|
+
delete: (id: string) => Promise<{
|
|
453
|
+
ok: true;
|
|
454
|
+
}>;
|
|
455
|
+
};
|
|
456
|
+
/** Scénarios d'appel (flow) : graphe de blocs, validation et simulation sans appel réel. */
|
|
457
|
+
readonly flows: {
|
|
458
|
+
list: () => Promise<Pick<Flow, "number" | "name" | "id" | "description" | "enabled">[]>;
|
|
459
|
+
get: (id: string) => Promise<Flow>;
|
|
460
|
+
create: (input: FlowInput) => Promise<Flow>;
|
|
461
|
+
update: (id: string, input: Partial<FlowInput>) => Promise<Flow>;
|
|
462
|
+
delete: (id: string) => Promise<{
|
|
463
|
+
ok: true;
|
|
464
|
+
}>;
|
|
465
|
+
validate: (graph: Flow["graph"]) => Promise<{
|
|
466
|
+
errors: string[];
|
|
467
|
+
warnings: string[];
|
|
468
|
+
}>;
|
|
469
|
+
simulate: (graph: Flow["graph"], opts?: {
|
|
470
|
+
caller?: string;
|
|
471
|
+
did?: string;
|
|
472
|
+
digits?: string[];
|
|
473
|
+
}) => Promise<Json>;
|
|
474
|
+
};
|
|
475
|
+
readonly queues: {
|
|
476
|
+
list: () => Promise<Json[]>;
|
|
477
|
+
create: (input: Json) => Promise<Json>;
|
|
478
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
479
|
+
delete: (id: string) => Promise<{
|
|
480
|
+
ok: true;
|
|
481
|
+
}>;
|
|
482
|
+
};
|
|
483
|
+
readonly ringGroups: {
|
|
484
|
+
list: () => Promise<Json[]>;
|
|
485
|
+
create: (input: Json) => Promise<Json>;
|
|
486
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
487
|
+
delete: (id: string) => Promise<{
|
|
488
|
+
ok: true;
|
|
489
|
+
}>;
|
|
490
|
+
};
|
|
491
|
+
readonly ivr: {
|
|
492
|
+
list: () => Promise<Json[]>;
|
|
493
|
+
create: (input: Json) => Promise<Json>;
|
|
494
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
495
|
+
delete: (id: string) => Promise<{
|
|
496
|
+
ok: true;
|
|
497
|
+
}>;
|
|
498
|
+
};
|
|
499
|
+
readonly timeConditions: {
|
|
500
|
+
list: () => Promise<Json[]>;
|
|
501
|
+
create: (input: Json) => Promise<Json>;
|
|
502
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
503
|
+
delete: (id: string) => Promise<{
|
|
504
|
+
ok: true;
|
|
505
|
+
}>;
|
|
506
|
+
};
|
|
507
|
+
readonly conferences: {
|
|
508
|
+
list: () => Promise<Json[]>;
|
|
509
|
+
create: (input: Json) => Promise<Json>;
|
|
510
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
511
|
+
delete: (id: string) => Promise<{
|
|
512
|
+
ok: true;
|
|
513
|
+
}>;
|
|
514
|
+
};
|
|
515
|
+
readonly reseller: {
|
|
516
|
+
me: () => Promise<Json>;
|
|
517
|
+
clients: {
|
|
518
|
+
list: (search?: string) => Promise<Json[]>;
|
|
519
|
+
get: (id: string) => Promise<Json>;
|
|
520
|
+
create: (input: ClientInput) => Promise<Json>;
|
|
521
|
+
/** Suspension, réactivation, prolongation d'essai. */
|
|
522
|
+
update: (id: string, input: {
|
|
523
|
+
status?: "ACTIVE" | "SUSPENDED";
|
|
524
|
+
extendTrialDays?: number;
|
|
525
|
+
}) => Promise<Json>;
|
|
526
|
+
subscribe: (id: string, input: Json) => Promise<Json>;
|
|
527
|
+
sms: (id: string) => Promise<{
|
|
528
|
+
credits: SmsCredits;
|
|
529
|
+
blocks: SenderBlock[];
|
|
530
|
+
}>;
|
|
531
|
+
/** MONTHLY remplace le forfait mensuel ; LIFETIME ajoute au solde à vie. */
|
|
532
|
+
applySmsPack: (id: string, packId: string) => Promise<SmsCredits>;
|
|
533
|
+
blockSender: (id: string, input: SenderBlockInput) => Promise<SenderBlock>;
|
|
534
|
+
};
|
|
535
|
+
catalog: () => Promise<Json>;
|
|
536
|
+
plans: {
|
|
537
|
+
list: () => Promise<Json[]>;
|
|
538
|
+
create: (input: Json) => Promise<Json>;
|
|
539
|
+
update: (id: string, input: Partial<Json>) => Promise<Json>;
|
|
540
|
+
delete: (id: string) => Promise<{
|
|
541
|
+
ok: true;
|
|
542
|
+
}>;
|
|
543
|
+
};
|
|
544
|
+
invoices: {
|
|
545
|
+
list: () => Promise<Json[]>;
|
|
546
|
+
validate: (id: string, input?: Json) => Promise<Json>;
|
|
547
|
+
cancel: (id: string, input?: Json) => Promise<Json>;
|
|
548
|
+
};
|
|
549
|
+
smsPacks: {
|
|
550
|
+
list: () => Promise<SmsPack[]>;
|
|
551
|
+
create: (input: SmsPackInput) => Promise<SmsPack>;
|
|
552
|
+
update: (id: string, input: Partial<SmsPackInput>) => Promise<SmsPack>;
|
|
553
|
+
delete: (id: string) => Promise<{
|
|
554
|
+
ok: true;
|
|
555
|
+
}>;
|
|
556
|
+
};
|
|
557
|
+
senderBlocks: {
|
|
558
|
+
list: () => Promise<SenderBlock[]>;
|
|
559
|
+
create: (input: SenderBlockInput) => Promise<SenderBlock>;
|
|
560
|
+
delete: (id: string) => Promise<{
|
|
561
|
+
ok: true;
|
|
562
|
+
}>;
|
|
563
|
+
};
|
|
564
|
+
/** SMS de tous vos clients. */
|
|
565
|
+
sms: (opts?: {
|
|
566
|
+
orgId?: string;
|
|
567
|
+
status?: string;
|
|
568
|
+
page?: number;
|
|
569
|
+
}) => Promise<{
|
|
570
|
+
total: number;
|
|
571
|
+
items: Json[];
|
|
572
|
+
}>;
|
|
573
|
+
/** Webhooks recevant les événements de tous vos clients (sms.status). */
|
|
574
|
+
webhooks: {
|
|
575
|
+
list: () => Promise<Webhook[]>;
|
|
576
|
+
create: (input: {
|
|
577
|
+
url: string;
|
|
578
|
+
events?: "sms.status"[];
|
|
579
|
+
enabled?: boolean;
|
|
580
|
+
}) => Promise<Webhook>;
|
|
581
|
+
update: (id: string, input: Partial<WebhookInput>) => Promise<Webhook>;
|
|
582
|
+
delete: (id: string) => Promise<{
|
|
583
|
+
ok: true;
|
|
584
|
+
}>;
|
|
585
|
+
};
|
|
586
|
+
};
|
|
269
587
|
}
|
package/dist/client.js
CHANGED
|
@@ -11,24 +11,29 @@ export class VoxNovaError extends Error {
|
|
|
11
11
|
this.name = 'VoxNovaError';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
const q = encodeURIComponent;
|
|
14
15
|
export class VoxNova {
|
|
15
16
|
opts;
|
|
16
17
|
base;
|
|
17
18
|
f;
|
|
18
19
|
constructor(opts) {
|
|
19
20
|
this.opts = opts;
|
|
20
|
-
if (!opts.apiKey?.startsWith('vxk_'))
|
|
21
|
-
throw new Error('Clé API vxk_… requise');
|
|
21
|
+
if (!opts.apiKey?.startsWith('vxk_') && !opts.apiKey?.startsWith('vxr_'))
|
|
22
|
+
throw new Error('Clé API vxk_… (entreprise) ou vxr_… (revendeur) requise');
|
|
22
23
|
this.base = opts.baseUrl.replace(/\/+$/, '');
|
|
23
24
|
this.f = opts.fetch ?? fetch;
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
+
/** Revendeur : client agissant au nom d'un de vos clients (même clé vxr_, en-tête X-Organization). */
|
|
27
|
+
forOrganization(organization) {
|
|
28
|
+
return new VoxNova({ ...this.opts, organization });
|
|
29
|
+
}
|
|
30
|
+
/** Requête brute vers l'API (chemin commençant par /api/). */
|
|
26
31
|
async request(method, path, body, query) {
|
|
27
32
|
const qs = query ? Object.entries(query).filter(([, v]) => v !== undefined).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&') : '';
|
|
28
33
|
const isForm = typeof FormData !== 'undefined' && body instanceof FormData;
|
|
29
34
|
const res = await this.f(`${this.base}${path}${qs ? `?${qs}` : ''}`, {
|
|
30
35
|
method,
|
|
31
|
-
headers: { Authorization: `Bearer ${this.opts.apiKey}`, Accept: 'application/json', ...(body && !isForm ? { 'Content-Type': 'application/json' } : {}) },
|
|
36
|
+
headers: { Authorization: `Bearer ${this.opts.apiKey}`, Accept: 'application/json', ...(this.opts.organization ? { 'X-Organization': this.opts.organization } : {}), ...(body && !isForm ? { 'Content-Type': 'application/json' } : {}) },
|
|
32
37
|
body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
|
|
33
38
|
signal: AbortSignal.timeout(this.opts.timeoutMs ?? 15000),
|
|
34
39
|
});
|
|
@@ -107,4 +112,83 @@ export class VoxNova {
|
|
|
107
112
|
logs = {
|
|
108
113
|
list: (opts = {}) => this.request('GET', '/api/v1/logs', undefined, opts),
|
|
109
114
|
};
|
|
115
|
+
// ---------------------------------------------------------------- SMS
|
|
116
|
+
sms = {
|
|
117
|
+
/** Met un SMS en file (statuts : webhook sms.status ou `get`). Refusé sans crédit ou si l'expéditeur est interdit. */
|
|
118
|
+
send: (input) => this.request('POST', '/api/v1/sms', input),
|
|
119
|
+
list: (opts = {}) => this.request('GET', '/api/v1/sms', undefined, opts),
|
|
120
|
+
/** Par identifiant ou messageId du fournisseur, avec les événements de remise (accepted, sent, delivered…). */
|
|
121
|
+
get: (id) => this.request('GET', `/api/v1/sms/${q(id)}`),
|
|
122
|
+
/** Forfait du mois (remis à zéro chaque mois) et solde à vie. */
|
|
123
|
+
balance: () => this.request('GET', '/api/v1/sms/balance'),
|
|
124
|
+
};
|
|
125
|
+
/** Webhooks de l'entreprise : call.completed, sms.status (signés, voir `verifyWebhook`). */
|
|
126
|
+
webhooks = {
|
|
127
|
+
list: () => this.request('GET', '/api/v1/webhooks'),
|
|
128
|
+
/** Le secret de signature n'est renvoyé qu'à la création. */
|
|
129
|
+
create: (input) => this.request('POST', '/api/v1/webhooks', input),
|
|
130
|
+
update: (id, input) => this.request('PATCH', `/api/v1/webhooks/${q(id)}`, input),
|
|
131
|
+
delete: (id) => this.request('DELETE', `/api/v1/webhooks/${q(id)}`),
|
|
132
|
+
};
|
|
133
|
+
// ---------------------------------------------------------------- panneau d'une entreprise (clé revendeur + organization)
|
|
134
|
+
/** Toute route du panneau (voir la documentation Swagger) : `vx.panel.get('/api/centrex/queues')`. */
|
|
135
|
+
panel = {
|
|
136
|
+
get: (path, query) => this.request('GET', path, undefined, query),
|
|
137
|
+
post: (path, body) => this.request('POST', path, body ?? {}),
|
|
138
|
+
put: (path, body) => this.request('PUT', path, body ?? {}),
|
|
139
|
+
patch: (path, body) => this.request('PATCH', path, body ?? {}),
|
|
140
|
+
delete: (path) => this.request('DELETE', path),
|
|
141
|
+
};
|
|
142
|
+
crud(base, update = 'PUT') {
|
|
143
|
+
return {
|
|
144
|
+
list: () => this.request('GET', base),
|
|
145
|
+
create: (input) => this.request('POST', base, input),
|
|
146
|
+
update: (id, input) => this.request(update, `${base}/${q(id)}`, input),
|
|
147
|
+
delete: (id) => this.request('DELETE', `${base}/${q(id)}`),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Utilisateurs de l'entreprise (invitation par e-mail, droits). */
|
|
151
|
+
users = { ...this.crud('/api/org/users', 'PATCH'), invite: (id) => this.request('POST', `/api/org/users/${q(id)}/invite`, {}), permissions: (id, permissions) => this.request('PUT', `/api/org/users/${q(id)}/permissions`, { permissions }) };
|
|
152
|
+
/** Postes (extensions) : création, modification complète, mot de passe SIP, état d'enregistrement. */
|
|
153
|
+
extensions = { ...this.crud('/api/org/extensions'), sipAccount: (id) => this.request('GET', `/api/org/extensions/${q(id)}/sip-account`), resetSipPassword: (id) => this.request('POST', `/api/org/extensions/${q(id)}/sip-password`, {}), status: (id) => this.request('GET', `/api/org/extensions/${q(id)}/status`) };
|
|
154
|
+
/** Scénarios d'appel (flow) : graphe de blocs, validation et simulation sans appel réel. */
|
|
155
|
+
flows = {
|
|
156
|
+
list: () => this.request('GET', '/api/centrex/flows'),
|
|
157
|
+
get: (id) => this.request('GET', `/api/centrex/flows/${q(id)}`),
|
|
158
|
+
create: (input) => this.request('POST', '/api/centrex/flows', input),
|
|
159
|
+
update: (id, input) => this.request('PUT', `/api/centrex/flows/${q(id)}`, input),
|
|
160
|
+
delete: (id) => this.request('DELETE', `/api/centrex/flows/${q(id)}`),
|
|
161
|
+
validate: (graph) => this.request('POST', '/api/centrex/flows/validate', { graph }),
|
|
162
|
+
simulate: (graph, opts = {}) => this.request('POST', '/api/centrex/flows/simulate', { graph, ...opts }),
|
|
163
|
+
};
|
|
164
|
+
queues = this.crud('/api/centrex/queues');
|
|
165
|
+
ringGroups = this.crud('/api/centrex/ring-groups');
|
|
166
|
+
ivr = this.crud('/api/centrex/ivr');
|
|
167
|
+
timeConditions = this.crud('/api/centrex/time-conditions');
|
|
168
|
+
conferences = this.crud('/api/centrex/conferences');
|
|
169
|
+
// ---------------------------------------------------------------- revendeur (clé vxr_)
|
|
170
|
+
reseller = {
|
|
171
|
+
me: () => this.request('GET', '/api/reseller/me'),
|
|
172
|
+
clients: {
|
|
173
|
+
list: (search) => this.request('GET', '/api/reseller/clients', undefined, { q: search }),
|
|
174
|
+
get: (id) => this.request('GET', `/api/reseller/clients/${q(id)}`),
|
|
175
|
+
create: (input) => this.request('POST', '/api/reseller/clients', input),
|
|
176
|
+
/** Suspension, réactivation, prolongation d'essai. */
|
|
177
|
+
update: (id, input) => this.request('PATCH', `/api/reseller/clients/${q(id)}`, input),
|
|
178
|
+
subscribe: (id, input) => this.request('POST', `/api/reseller/clients/${q(id)}/subscription`, input),
|
|
179
|
+
sms: (id) => this.request('GET', `/api/reseller/clients/${q(id)}/sms`),
|
|
180
|
+
/** MONTHLY remplace le forfait mensuel ; LIFETIME ajoute au solde à vie. */
|
|
181
|
+
applySmsPack: (id, packId) => this.request('POST', `/api/reseller/clients/${q(id)}/sms/pack`, { packId }),
|
|
182
|
+
blockSender: (id, input) => this.request('POST', `/api/reseller/clients/${q(id)}/sms/sender-blocks`, input),
|
|
183
|
+
},
|
|
184
|
+
catalog: () => this.request('GET', '/api/reseller/catalog'),
|
|
185
|
+
plans: this.crud('/api/reseller/plans'),
|
|
186
|
+
invoices: { list: () => this.request('GET', '/api/reseller/invoices'), validate: (id, input = {}) => this.request('POST', `/api/reseller/invoices/${q(id)}/validate`, input), cancel: (id, input = {}) => this.request('POST', `/api/reseller/invoices/${q(id)}/cancel`, input) },
|
|
187
|
+
smsPacks: this.crud('/api/reseller/sms/packs'),
|
|
188
|
+
senderBlocks: { list: () => this.request('GET', '/api/reseller/sms/sender-blocks'), create: (input) => this.request('POST', '/api/reseller/sms/sender-blocks', input), delete: (id) => this.request('DELETE', `/api/reseller/sms/sender-blocks/${q(id)}`) },
|
|
189
|
+
/** SMS de tous vos clients. */
|
|
190
|
+
sms: (opts = {}) => this.request('GET', '/api/reseller/sms', undefined, opts),
|
|
191
|
+
/** Webhooks recevant les événements de tous vos clients (sms.status). */
|
|
192
|
+
webhooks: { list: () => this.request('GET', '/api/reseller/webhooks'), create: (input) => this.request('POST', '/api/reseller/webhooks', input), update: (id, input) => this.request('PATCH', `/api/reseller/webhooks/${q(id)}`, input), delete: (id) => this.request('DELETE', `/api/reseller/webhooks/${q(id)}`) },
|
|
193
|
+
};
|
|
110
194
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Événements des webhooks d'entreprise ou de revendeur (call.completed, sms.status) : en-têtes
|
|
3
|
+
* `X-VoxNova-Event` et `X-VoxNova-Signature: sha256=<HMAC-SHA256 du corps brut avec le secret du webhook>`.
|
|
4
|
+
* (Les rappels des applications vocales utilisent `verifySignature`, au format `t=…,v1=…`.)
|
|
5
|
+
*/
|
|
6
|
+
export interface SmsStatusEvent {
|
|
7
|
+
event: 'sms.status';
|
|
8
|
+
createdAt: string;
|
|
9
|
+
data: {
|
|
10
|
+
id: string;
|
|
11
|
+
orgId: string;
|
|
12
|
+
to: string;
|
|
13
|
+
status: string;
|
|
14
|
+
messageId: string | null;
|
|
15
|
+
tag: string | null;
|
|
16
|
+
type: string | null;
|
|
17
|
+
segments: number;
|
|
18
|
+
sender: string | null;
|
|
19
|
+
error: string | null;
|
|
20
|
+
at: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare function verifyWebhook(secret: string, rawBody: string | Buffer, header: string | undefined | null): boolean;
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export function verifyWebhook(secret, rawBody, header) {
|
|
3
|
+
if (!header?.startsWith('sha256='))
|
|
4
|
+
return false;
|
|
5
|
+
const expected = createHmac('sha256', secret).update(rawBody).digest();
|
|
6
|
+
const got = Buffer.from(header.slice(7), 'hex');
|
|
7
|
+
return got.length === expected.length && timingSafeEqual(got, expected);
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voxnova/sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "SDK TypeScript de la plateforme
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "SDK TypeScript de la plateforme VoxNova : API REST (SMS, endpoints, numéros, appels, scénarios d'appel, espace revendeur), instructions vocales, vérification des webhooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
20
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
21
|
+
"test": "node --test test/*.test.mjs"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
24
|
"node": ">=18"
|