@fastermessage/sdk 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +126 -2
- package/dist/cjs/client.d.ts +34 -0
- package/dist/cjs/client.js +52 -0
- package/dist/cjs/errors.d.ts +59 -0
- package/dist/cjs/errors.js +77 -0
- package/dist/cjs/http.d.ts +30 -0
- package/dist/cjs/http.js +156 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/resources.d.ts +60 -0
- package/dist/cjs/resources.js +131 -0
- package/dist/cjs/types.d.ts +204 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/webhook.d.ts +11 -0
- package/dist/cjs/webhook.js +21 -0
- package/dist/esm/client.d.ts +34 -0
- package/dist/esm/client.js +48 -0
- package/dist/esm/errors.d.ts +59 -0
- package/dist/esm/errors.js +62 -0
- package/dist/esm/http.d.ts +30 -0
- package/dist/esm/http.js +152 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/resources.d.ts +60 -0
- package/dist/esm/resources.js +121 -0
- package/dist/esm/types.d.ts +204 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/webhook.d.ts +11 -0
- package/dist/esm/webhook.js +18 -0
- package/package.json +29 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Urban Technology
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,127 @@
|
|
|
1
|
-
#
|
|
1
|
+
# FasterMessage Node.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SDK Node.js officiel de l'API FasterMessage v2 : SMS, e-mail, codes de vérification (OTP), contacts et webhooks.
|
|
4
|
+
|
|
5
|
+
Node.js 20.3 ou plus récent. Aucune dépendance. Types TypeScript inclus, en ESM comme en CommonJS.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fastermessage/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> Côté serveur seulement : le client porte vos identifiants et refuse de démarrer dans un navigateur.
|
|
12
|
+
|
|
13
|
+
## Démarrage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { FasterMessage } from '@fastermessage/sdk';
|
|
17
|
+
// CommonJS : const { FasterMessage } = require('@fastermessage/sdk');
|
|
18
|
+
|
|
19
|
+
const fm = new FasterMessage({
|
|
20
|
+
username: process.env.FM_API_USER,
|
|
21
|
+
password: process.env.FM_API_PASSWORD,
|
|
22
|
+
apiKey: process.env.FM_API_KEY, // fm_test_… simule, fm_live_… envoie et débite
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const message = await fm.sms.send({
|
|
26
|
+
to: '+2290160000000',
|
|
27
|
+
from: 'FASTERMSG',
|
|
28
|
+
content: 'Votre commande est prete.',
|
|
29
|
+
client_reference: 'cmd-42',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(message.id, message.status); // queued
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Sur le domaine d'API de votre marque : `new FasterMessage({ ..., baseUrl: 'https://api.votre-marque.com' })`.
|
|
36
|
+
|
|
37
|
+
## Ce que fait le SDK
|
|
38
|
+
|
|
39
|
+
| Besoin | Appel |
|
|
40
|
+
|---|---|
|
|
41
|
+
| SMS | `fm.sms.send()`, `fm.sms.sendBatch()` (500 lignes) |
|
|
42
|
+
| E-mail | `fm.email.send()` |
|
|
43
|
+
| Canal en paramètre | `fm.messages.send()` |
|
|
44
|
+
| Suivi | `fm.messages.get()`, `list()`, `all()` |
|
|
45
|
+
| OTP | `fm.otp.request()`, `fm.otp.verify()` |
|
|
46
|
+
| Contacts | `fm.contacts`, `fm.contactGroups` : `create`, `get`, `list`, `all`, `update`, `delete` |
|
|
47
|
+
| Compte | `fm.account.balance()`, `credits()`, `allCredits()` |
|
|
48
|
+
| Webhooks | `Webhook.isGenuine()` |
|
|
49
|
+
|
|
50
|
+
Les paramètres et les réponses gardent les noms de champs de l'API (`client_reference`, `created_at`) : un champ ajouté par l'API est disponible sans mise à jour du SDK. La référence complète est dans la [documentation de l'API](https://www.fastermessage.com/docs).
|
|
51
|
+
|
|
52
|
+
`all()` parcourt toutes les pages et ne demande la suivante que si vous continuez :
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
for await (const contact of fm.contacts.all({ group_id: 'g_123' })) {
|
|
56
|
+
console.log(contact.phone_number);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
WhatsApp et la voix ne sont pas encore servis par l'API v2.
|
|
61
|
+
|
|
62
|
+
## Rejeux sans double envoi
|
|
63
|
+
|
|
64
|
+
Chaque écriture porte une clé `Idempotency-Key` générée. Le SDK rejoue seul, avec la même clé et deux fois au plus, une coupure réseau, un `5xx` (sauf `501`) et un `429` qui annonce `Retry-After`.
|
|
65
|
+
|
|
66
|
+
Passez votre propre clé pour survivre à un redémarrage de votre programme :
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
await fm.sms.send(params, { idempotencyKey: 'commande-42-confirmation' });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Erreurs
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { ApiError, ConnectionError, InsufficientBalanceError } from '@fastermessage/sdk';
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await fm.sms.send(params);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof InsufficientBalanceError) {
|
|
81
|
+
// Rechargez le compte débité.
|
|
82
|
+
} else if (error instanceof ApiError) {
|
|
83
|
+
// Testez code, stable, et non le message.
|
|
84
|
+
console.error(error.status, error.code, error.param, error.requestId);
|
|
85
|
+
} else if (error instanceof ConnectionError) {
|
|
86
|
+
// L'appel a pu aboutir : rejouez avec error.idempotencyKey, jamais avec une nouvelle clé.
|
|
87
|
+
} else {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Erreurs : `InvalidRequestError` (400), `AuthenticationError` (401), `InsufficientBalanceError` (402), `PermissionError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429), `NotImplementedError` (501), `ServerError` (5xx).
|
|
94
|
+
|
|
95
|
+
Dans un lot, lisez toujours `errors` : un retour normal ne garantit pas que toutes les lignes soient parties.
|
|
96
|
+
|
|
97
|
+
## Webhooks
|
|
98
|
+
|
|
99
|
+
Vérifiez la signature sur le corps **brut**, avant tout décodage JSON. Avec Express :
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import express from 'express';
|
|
103
|
+
import { Webhook } from '@fastermessage/sdk';
|
|
104
|
+
|
|
105
|
+
const app = express();
|
|
106
|
+
|
|
107
|
+
app.post('/webhooks/fastermessage', express.raw({ type: 'application/json' }), (req, res) => {
|
|
108
|
+
const genuine = Webhook.isGenuine(req.body, req.get('X-Fastermessage-Signature'), process.env.FM_WEBHOOK_SECRET);
|
|
109
|
+
if (!genuine) return res.sendStatus(401);
|
|
110
|
+
|
|
111
|
+
res.sendStatus(200); // Répondez sous 10 secondes, traitez ensuite.
|
|
112
|
+
const event = JSON.parse(req.body);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Générez le secret dans « Intégration & API ». Le rappel SMS a le format `uuid`, `level`, `status` ; le rappel e-mail `id`, `status`, `error`.
|
|
117
|
+
|
|
118
|
+
## Tests
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install
|
|
122
|
+
npm test
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Licence
|
|
126
|
+
|
|
127
|
+
MIT.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Account, ContactGroups, Contacts, Email, Messages, OtpResource, Sms } from './resources.js';
|
|
2
|
+
export declare const DEFAULT_BASE_URL = "https://api.fastermessage.com";
|
|
3
|
+
export interface FasterMessageOptions {
|
|
4
|
+
/** Identifiant d'API, compte débité. */
|
|
5
|
+
username: string;
|
|
6
|
+
/** Mot de passe d'API. */
|
|
7
|
+
password: string;
|
|
8
|
+
/** Clé `fm_live_…` (réel) ou `fm_test_…` (simulé). */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
/** Le domaine d'API de votre marque, le cas échéant. */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Délai de chaque tentative, en millisecondes. 30 000 par défaut. */
|
|
13
|
+
timeout?: number;
|
|
14
|
+
/** Nombre de reprises après la première tentative. 2 par défaut. */
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
/** Remplace `fetch`, par exemple pour passer par un proxy ou pour vos tests. */
|
|
17
|
+
fetch?: typeof globalThis.fetch;
|
|
18
|
+
/** Remplace l'attente entre deux reprises, surtout utile dans vos tests. */
|
|
19
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** Point d'entrée du SDK. Côté serveur seulement : il porte vos identifiants. */
|
|
22
|
+
export declare class FasterMessage {
|
|
23
|
+
#private;
|
|
24
|
+
readonly sms: Sms;
|
|
25
|
+
readonly email: Email;
|
|
26
|
+
readonly messages: Messages;
|
|
27
|
+
readonly otp: OtpResource;
|
|
28
|
+
readonly contacts: Contacts;
|
|
29
|
+
readonly contactGroups: ContactGroups;
|
|
30
|
+
readonly account: Account;
|
|
31
|
+
constructor(options: FasterMessageOptions);
|
|
32
|
+
/** Vrai avec une clé de test : messages et OTP simulés, mais contacts et soldes réels. */
|
|
33
|
+
get testMode(): boolean;
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FasterMessage = exports.DEFAULT_BASE_URL = void 0;
|
|
4
|
+
const http_js_1 = require("./http.js");
|
|
5
|
+
const resources_js_1 = require("./resources.js");
|
|
6
|
+
exports.DEFAULT_BASE_URL = 'https://api.fastermessage.com';
|
|
7
|
+
/** Point d'entrée du SDK. Côté serveur seulement : il porte vos identifiants. */
|
|
8
|
+
class FasterMessage {
|
|
9
|
+
sms;
|
|
10
|
+
email;
|
|
11
|
+
messages;
|
|
12
|
+
otp;
|
|
13
|
+
contacts;
|
|
14
|
+
contactGroups;
|
|
15
|
+
account;
|
|
16
|
+
#testMode;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
const { username, password, apiKey } = options ?? {};
|
|
19
|
+
if (!username || !password || !apiKey) {
|
|
20
|
+
throw new TypeError("L'identifiant, le mot de passe et la clé d'API sont tous obligatoires.");
|
|
21
|
+
}
|
|
22
|
+
if ('window' in globalThis && 'document' in globalThis) {
|
|
23
|
+
throw new Error('Le SDK FasterMessage ne s’utilise pas dans un navigateur : vos identifiants y seraient publics.');
|
|
24
|
+
}
|
|
25
|
+
this.#testMode = apiKey.includes('_test_');
|
|
26
|
+
const http = new http_js_1.Requester({
|
|
27
|
+
authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
|
|
28
|
+
apiKey,
|
|
29
|
+
baseUrl: (options.baseUrl ?? exports.DEFAULT_BASE_URL).replace(/\/+$/, ''),
|
|
30
|
+
timeout: options.timeout ?? 30_000,
|
|
31
|
+
maxRetries: Math.max(0, options.maxRetries ?? 2),
|
|
32
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
33
|
+
...(options.sleep ? { sleep: options.sleep } : {}),
|
|
34
|
+
});
|
|
35
|
+
this.sms = new resources_js_1.Sms(http);
|
|
36
|
+
this.email = new resources_js_1.Email(http);
|
|
37
|
+
this.messages = new resources_js_1.Messages(http);
|
|
38
|
+
this.otp = new resources_js_1.OtpResource(http);
|
|
39
|
+
this.contacts = new resources_js_1.Contacts(http);
|
|
40
|
+
this.contactGroups = new resources_js_1.ContactGroups(http);
|
|
41
|
+
this.account = new resources_js_1.Account(http);
|
|
42
|
+
}
|
|
43
|
+
/** Vrai avec une clé de test : messages et OTP simulés, mais contacts et soldes réels. */
|
|
44
|
+
get testMode() {
|
|
45
|
+
return this.#testMode;
|
|
46
|
+
}
|
|
47
|
+
/** Évite d'écrire le moindre détail du client dans un journal par `console.log`. */
|
|
48
|
+
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
49
|
+
return `FasterMessage { testMode: ${this.#testMode} }`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.FasterMessage = FasterMessage;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Toute erreur levée par le SDK. */
|
|
2
|
+
export declare class FasterMessageError extends Error {
|
|
3
|
+
constructor(message: string, options?: ErrorOptions);
|
|
4
|
+
}
|
|
5
|
+
export interface ApiErrorDetails {
|
|
6
|
+
status: number;
|
|
7
|
+
code: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
param?: string | null;
|
|
10
|
+
requestId?: string | null;
|
|
11
|
+
retryAfter?: number | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Refus de l'API. Testez `code`, qui est stable : `message` est un libellé en
|
|
15
|
+
* anglais, susceptible d'être reformulé.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ApiError extends FasterMessageError {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly type: string;
|
|
21
|
+
readonly param: string | null;
|
|
22
|
+
readonly requestId: string | null;
|
|
23
|
+
readonly retryAfter: number | null;
|
|
24
|
+
constructor(message: string, details: ApiErrorDetails);
|
|
25
|
+
}
|
|
26
|
+
/** 400 : la requête est à corriger. */
|
|
27
|
+
export declare class InvalidRequestError extends ApiError {
|
|
28
|
+
}
|
|
29
|
+
/** 401 : identifiant, mot de passe ou clé refusés. */
|
|
30
|
+
export declare class AuthenticationError extends ApiError {
|
|
31
|
+
}
|
|
32
|
+
/** 402 : solde insuffisant, le compte débité est à recharger. */
|
|
33
|
+
export declare class InsufficientBalanceError extends ApiError {
|
|
34
|
+
}
|
|
35
|
+
/** 403 : opération interdite à ce compte. */
|
|
36
|
+
export declare class PermissionError extends ApiError {
|
|
37
|
+
}
|
|
38
|
+
/** 404 : ressource introuvable. */
|
|
39
|
+
export declare class NotFoundError extends ApiError {
|
|
40
|
+
}
|
|
41
|
+
/** 409 : conflit, par exemple un groupe non vide ou une clé d'idempotence réutilisée. */
|
|
42
|
+
export declare class ConflictError extends ApiError {
|
|
43
|
+
}
|
|
44
|
+
/** 429 : débit dépassé, voir `retryAfter`. */
|
|
45
|
+
export declare class RateLimitError extends ApiError {
|
|
46
|
+
}
|
|
47
|
+
/** 501 : fonction déclarée au contrat mais pas encore servie. */
|
|
48
|
+
export declare class NotImplementedError extends ApiError {
|
|
49
|
+
}
|
|
50
|
+
/** 5xx : panne côté API. */
|
|
51
|
+
export declare class ServerError extends ApiError {
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Coupure réseau ou délai dépassé. L'appel a pu aboutir : rejouez avec
|
|
55
|
+
* `idempotencyKey`, jamais avec une nouvelle clé.
|
|
56
|
+
*/
|
|
57
|
+
export declare class ConnectionError extends FasterMessageError {
|
|
58
|
+
idempotencyKey: string | null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionError = exports.ServerError = exports.NotImplementedError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionError = exports.InsufficientBalanceError = exports.AuthenticationError = exports.InvalidRequestError = exports.ApiError = exports.FasterMessageError = void 0;
|
|
4
|
+
/** Toute erreur levée par le SDK. */
|
|
5
|
+
class FasterMessageError extends Error {
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.FasterMessageError = FasterMessageError;
|
|
12
|
+
/**
|
|
13
|
+
* Refus de l'API. Testez `code`, qui est stable : `message` est un libellé en
|
|
14
|
+
* anglais, susceptible d'être reformulé.
|
|
15
|
+
*/
|
|
16
|
+
class ApiError extends FasterMessageError {
|
|
17
|
+
status;
|
|
18
|
+
code;
|
|
19
|
+
type;
|
|
20
|
+
param;
|
|
21
|
+
requestId;
|
|
22
|
+
retryAfter;
|
|
23
|
+
constructor(message, details) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = details.status;
|
|
26
|
+
this.code = details.code;
|
|
27
|
+
this.type = details.type ?? '';
|
|
28
|
+
this.param = details.param ?? null;
|
|
29
|
+
this.requestId = details.requestId ?? null;
|
|
30
|
+
this.retryAfter = details.retryAfter ?? null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.ApiError = ApiError;
|
|
34
|
+
/** 400 : la requête est à corriger. */
|
|
35
|
+
class InvalidRequestError extends ApiError {
|
|
36
|
+
}
|
|
37
|
+
exports.InvalidRequestError = InvalidRequestError;
|
|
38
|
+
/** 401 : identifiant, mot de passe ou clé refusés. */
|
|
39
|
+
class AuthenticationError extends ApiError {
|
|
40
|
+
}
|
|
41
|
+
exports.AuthenticationError = AuthenticationError;
|
|
42
|
+
/** 402 : solde insuffisant, le compte débité est à recharger. */
|
|
43
|
+
class InsufficientBalanceError extends ApiError {
|
|
44
|
+
}
|
|
45
|
+
exports.InsufficientBalanceError = InsufficientBalanceError;
|
|
46
|
+
/** 403 : opération interdite à ce compte. */
|
|
47
|
+
class PermissionError extends ApiError {
|
|
48
|
+
}
|
|
49
|
+
exports.PermissionError = PermissionError;
|
|
50
|
+
/** 404 : ressource introuvable. */
|
|
51
|
+
class NotFoundError extends ApiError {
|
|
52
|
+
}
|
|
53
|
+
exports.NotFoundError = NotFoundError;
|
|
54
|
+
/** 409 : conflit, par exemple un groupe non vide ou une clé d'idempotence réutilisée. */
|
|
55
|
+
class ConflictError extends ApiError {
|
|
56
|
+
}
|
|
57
|
+
exports.ConflictError = ConflictError;
|
|
58
|
+
/** 429 : débit dépassé, voir `retryAfter`. */
|
|
59
|
+
class RateLimitError extends ApiError {
|
|
60
|
+
}
|
|
61
|
+
exports.RateLimitError = RateLimitError;
|
|
62
|
+
/** 501 : fonction déclarée au contrat mais pas encore servie. */
|
|
63
|
+
class NotImplementedError extends ApiError {
|
|
64
|
+
}
|
|
65
|
+
exports.NotImplementedError = NotImplementedError;
|
|
66
|
+
/** 5xx : panne côté API. */
|
|
67
|
+
class ServerError extends ApiError {
|
|
68
|
+
}
|
|
69
|
+
exports.ServerError = ServerError;
|
|
70
|
+
/**
|
|
71
|
+
* Coupure réseau ou délai dépassé. L'appel a pu aboutir : rejouez avec
|
|
72
|
+
* `idempotencyKey`, jamais avec une nouvelle clé.
|
|
73
|
+
*/
|
|
74
|
+
class ConnectionError extends FasterMessageError {
|
|
75
|
+
idempotencyKey = null;
|
|
76
|
+
}
|
|
77
|
+
exports.ConnectionError = ConnectionError;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const VERSION = "0.1.0";
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
/** Votre clé, pour survivre à un redémarrage de votre programme. */
|
|
4
|
+
idempotencyKey?: string;
|
|
5
|
+
/** Repris par `X-Request-Id`, pour relier l'appel à vos journaux. */
|
|
6
|
+
requestId?: string;
|
|
7
|
+
/** Annule l'appel. Une annulation n'est jamais rejouée. */
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface RequesterConfig {
|
|
11
|
+
authorization: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
timeout: number;
|
|
15
|
+
maxRetries: number;
|
|
16
|
+
fetch?: typeof globalThis.fetch;
|
|
17
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
interface Call {
|
|
20
|
+
query?: object;
|
|
21
|
+
body?: object;
|
|
22
|
+
options?: RequestOptions | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Le chemin unique de tous les appels : en-têtes, idempotence, reprises, erreurs. */
|
|
25
|
+
export declare class Requester {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(config: RequesterConfig);
|
|
28
|
+
request<T>(method: string, path: string, { query, body, options }?: Call): Promise<T>;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
package/dist/cjs/http.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Requester = exports.VERSION = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
6
|
+
exports.VERSION = '0.1.0';
|
|
7
|
+
/** Un même appel rejoué garde sa clé : c'est ce qui rend une reprise sans risque. */
|
|
8
|
+
const MUTATING = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
|
|
9
|
+
/** Le chemin unique de tous les appels : en-têtes, idempotence, reprises, erreurs. */
|
|
10
|
+
class Requester {
|
|
11
|
+
#config;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.#config = config;
|
|
14
|
+
}
|
|
15
|
+
async request(method, path, { query, body, options = {} } = {}) {
|
|
16
|
+
const url = this.#config.baseUrl + path + queryString(query);
|
|
17
|
+
const headers = {
|
|
18
|
+
Authorization: this.#config.authorization,
|
|
19
|
+
'X-Api-Key': this.#config.apiKey,
|
|
20
|
+
Accept: 'application/json',
|
|
21
|
+
'User-Agent': `fastermessage-node/${exports.VERSION}`,
|
|
22
|
+
};
|
|
23
|
+
if (body !== undefined)
|
|
24
|
+
headers['Content-Type'] = 'application/json';
|
|
25
|
+
if (options.requestId !== undefined)
|
|
26
|
+
headers['X-Request-Id'] = options.requestId;
|
|
27
|
+
let idempotencyKey = null;
|
|
28
|
+
if (MUTATING.has(method)) {
|
|
29
|
+
idempotencyKey = options.idempotencyKey ?? `sdk-${(0, node_crypto_1.randomBytes)(16).toString('hex')}`;
|
|
30
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
31
|
+
}
|
|
32
|
+
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
33
|
+
const sleep = this.#config.sleep ?? defaultSleep;
|
|
34
|
+
for (let attempt = 0;; attempt++) {
|
|
35
|
+
let response;
|
|
36
|
+
let text;
|
|
37
|
+
try {
|
|
38
|
+
const timeout = AbortSignal.timeout(this.#config.timeout);
|
|
39
|
+
const fetchFn = this.#config.fetch ?? globalThis.fetch;
|
|
40
|
+
response = await fetchFn(url, {
|
|
41
|
+
method,
|
|
42
|
+
headers,
|
|
43
|
+
...(payload === undefined ? {} : { body: payload }),
|
|
44
|
+
signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
|
|
45
|
+
});
|
|
46
|
+
text = await response.text();
|
|
47
|
+
}
|
|
48
|
+
catch (cause) {
|
|
49
|
+
if (options.signal?.aborted)
|
|
50
|
+
throw options.signal.reason;
|
|
51
|
+
if (attempt >= this.#config.maxRetries) {
|
|
52
|
+
const error = new errors_js_1.ConnectionError(`Connexion à l'API impossible : ${describe(cause)}`, { cause });
|
|
53
|
+
error.idempotencyKey = idempotencyKey;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
await sleep(backoff(attempt));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (response.status < 400)
|
|
60
|
+
return decode(response, text);
|
|
61
|
+
const failure = toError(response, text);
|
|
62
|
+
const wait = retryDelay(failure, attempt);
|
|
63
|
+
if (wait === null || attempt >= this.#config.maxRetries)
|
|
64
|
+
throw failure;
|
|
65
|
+
await sleep(wait);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.Requester = Requester;
|
|
70
|
+
function queryString(query) {
|
|
71
|
+
if (!query)
|
|
72
|
+
return '';
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
for (const [key, value] of Object.entries(query)) {
|
|
75
|
+
if (value !== undefined && value !== null)
|
|
76
|
+
params.append(key, String(value));
|
|
77
|
+
}
|
|
78
|
+
const encoded = params.toString();
|
|
79
|
+
return encoded ? `?${encoded}` : '';
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Reprise seulement si la même requête peut réussir plus tard. Un 429 sans
|
|
83
|
+
* `Retry-After` est le plafond OTP par numéro, à l'échelle de l'heure : le rejouer
|
|
84
|
+
* ne servirait à rien. Le 501 dit une fonction non servie, jamais une panne.
|
|
85
|
+
*/
|
|
86
|
+
function retryDelay(failure, attempt) {
|
|
87
|
+
if (failure.status === 429)
|
|
88
|
+
return failure.retryAfter === null ? null : failure.retryAfter * 1000;
|
|
89
|
+
if (failure.code === 'idempotency_key_in_flight')
|
|
90
|
+
return backoff(attempt);
|
|
91
|
+
if (failure.status >= 500 && failure.status !== 501)
|
|
92
|
+
return backoff(attempt);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function backoff(attempt) {
|
|
96
|
+
return Math.min(8000, 500 * 2 ** attempt) * (1 + Math.random() * 0.25);
|
|
97
|
+
}
|
|
98
|
+
function defaultSleep(milliseconds) {
|
|
99
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
100
|
+
}
|
|
101
|
+
function describe(cause) {
|
|
102
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
103
|
+
}
|
|
104
|
+
function decode(response, text) {
|
|
105
|
+
if (response.status === 204 || text.trim() === '')
|
|
106
|
+
return undefined;
|
|
107
|
+
const data = parse(text);
|
|
108
|
+
if (typeof data !== 'object' || data === null) {
|
|
109
|
+
throw new errors_js_1.ServerError("Réponse illisible de l'API.", {
|
|
110
|
+
status: response.status,
|
|
111
|
+
code: 'invalid_response',
|
|
112
|
+
type: 'api_error',
|
|
113
|
+
requestId: response.headers.get('x-request-id'),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
function parse(text) {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(text);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function stringOrNull(value) {
|
|
127
|
+
return typeof value === 'string' ? value : null;
|
|
128
|
+
}
|
|
129
|
+
function toError(response, text) {
|
|
130
|
+
const decoded = parse(text);
|
|
131
|
+
const error = decoded && typeof decoded.error === 'object' && decoded.error !== null ? decoded.error : {};
|
|
132
|
+
const status = response.status;
|
|
133
|
+
const retryAfter = response.headers.get('retry-after');
|
|
134
|
+
const ErrorClass = errorClass(status);
|
|
135
|
+
return new ErrorClass(stringOrNull(error.message) ?? `Erreur HTTP ${status}.`, {
|
|
136
|
+
status,
|
|
137
|
+
code: stringOrNull(error.code) ?? `http_${status}`,
|
|
138
|
+
type: stringOrNull(error.type) ?? '',
|
|
139
|
+
param: stringOrNull(error.param),
|
|
140
|
+
requestId: stringOrNull(error.request_id) ?? response.headers.get('x-request-id'),
|
|
141
|
+
retryAfter: retryAfter !== null && /^\d+$/.test(retryAfter.trim()) ? Number(retryAfter) : null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function errorClass(status) {
|
|
145
|
+
switch (status) {
|
|
146
|
+
case 400: return errors_js_1.InvalidRequestError;
|
|
147
|
+
case 401: return errors_js_1.AuthenticationError;
|
|
148
|
+
case 402: return errors_js_1.InsufficientBalanceError;
|
|
149
|
+
case 403: return errors_js_1.PermissionError;
|
|
150
|
+
case 404: return errors_js_1.NotFoundError;
|
|
151
|
+
case 409: return errors_js_1.ConflictError;
|
|
152
|
+
case 429: return errors_js_1.RateLimitError;
|
|
153
|
+
case 501: return errors_js_1.NotImplementedError;
|
|
154
|
+
default: return status >= 500 ? errors_js_1.ServerError : errors_js_1.ApiError;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_BASE_URL, FasterMessage } from './client.js';
|
|
2
|
+
export type { FasterMessageOptions } from './client.js';
|
|
3
|
+
export * from './errors.js';
|
|
4
|
+
export { VERSION } from './http.js';
|
|
5
|
+
export type { RequestOptions } from './http.js';
|
|
6
|
+
export { Webhook } from './webhook.js';
|
|
7
|
+
export type * from './types.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Webhook = exports.VERSION = exports.FasterMessage = exports.DEFAULT_BASE_URL = void 0;
|
|
18
|
+
var client_js_1 = require("./client.js");
|
|
19
|
+
Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return client_js_1.DEFAULT_BASE_URL; } });
|
|
20
|
+
Object.defineProperty(exports, "FasterMessage", { enumerable: true, get: function () { return client_js_1.FasterMessage; } });
|
|
21
|
+
__exportStar(require("./errors.js"), exports);
|
|
22
|
+
var http_js_1 = require("./http.js");
|
|
23
|
+
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return http_js_1.VERSION; } });
|
|
24
|
+
var webhook_js_1 = require("./webhook.js");
|
|
25
|
+
Object.defineProperty(exports, "Webhook", { enumerable: true, get: function () { return webhook_js_1.Webhook; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "commonjs" }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { RequestOptions, Requester } from './http.js';
|
|
2
|
+
import type { Balance, Contact, ContactBatchResult, ContactGroup, ContactQuery, CreateContactBatchParams, CreateContactGroupParams, CreateContactParams, CreditMovement, CreditQuery, Message, MessageQuery, Otp, Page, PageQuery, RequestOtpParams, SendEmailParams, SendMessageParams, SendSmsBatchParams, SendSmsParams, SmsBatchResult, UpdateContactGroupParams, UpdateContactParams } from './types.js';
|
|
3
|
+
export declare class Sms {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(http: Requester);
|
|
6
|
+
send(params: SendSmsParams, options?: RequestOptions): Promise<Message>;
|
|
7
|
+
/** Jusqu'à 500 lignes. Lisez toujours `errors` : un retour normal ne garantit pas que toutes soient parties. */
|
|
8
|
+
sendBatch(params: SendSmsBatchParams, options?: RequestOptions): Promise<SmsBatchResult>;
|
|
9
|
+
}
|
|
10
|
+
export declare class Email {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(http: Requester);
|
|
13
|
+
send(params: SendEmailParams, options?: RequestOptions): Promise<Message>;
|
|
14
|
+
}
|
|
15
|
+
export declare class Messages {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(http: Requester);
|
|
18
|
+
/** Envoi avec le canal en paramètre. WhatsApp et la voix répondent aujourd'hui `501`. */
|
|
19
|
+
send(params: SendMessageParams, options?: RequestOptions): Promise<Message>;
|
|
20
|
+
get(id: string): Promise<Message>;
|
|
21
|
+
list(query?: MessageQuery): Promise<Page<Message>>;
|
|
22
|
+
all(query?: MessageQuery): AsyncGenerator<Message, void, undefined>;
|
|
23
|
+
}
|
|
24
|
+
export declare class OtpResource {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(http: Requester);
|
|
27
|
+
request(params: RequestOtpParams, options?: RequestOptions): Promise<Otp>;
|
|
28
|
+
verify(id: string, code: string): Promise<Otp>;
|
|
29
|
+
}
|
|
30
|
+
export declare class Contacts {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(http: Requester);
|
|
33
|
+
create(params: CreateContactParams): Promise<Contact>;
|
|
34
|
+
/** Crée jusqu'à 500 contacts dans un groupe. */
|
|
35
|
+
createBatch(params: CreateContactBatchParams, options?: RequestOptions): Promise<ContactBatchResult>;
|
|
36
|
+
get(id: string): Promise<Contact>;
|
|
37
|
+
list(query?: ContactQuery): Promise<Page<Contact>>;
|
|
38
|
+
all(query?: ContactQuery): AsyncGenerator<Contact, void, undefined>;
|
|
39
|
+
update(id: string, params: UpdateContactParams): Promise<Contact>;
|
|
40
|
+
/** Suppression définitive. Ce n'est pas un désabonnement : pour cela, `consent: 'out'`. */
|
|
41
|
+
delete(id: string): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export declare class ContactGroups {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(http: Requester);
|
|
46
|
+
create(params: CreateContactGroupParams): Promise<ContactGroup>;
|
|
47
|
+
get(id: string): Promise<ContactGroup>;
|
|
48
|
+
list(query?: PageQuery): Promise<Page<ContactGroup>>;
|
|
49
|
+
all(query?: PageQuery): AsyncGenerator<ContactGroup, void, undefined>;
|
|
50
|
+
update(id: string, params: UpdateContactGroupParams): Promise<ContactGroup>;
|
|
51
|
+
/** Refusé en `409` tant que le groupe contient des contacts. */
|
|
52
|
+
delete(id: string): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export declare class Account {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(http: Requester);
|
|
57
|
+
balance(): Promise<Balance[]>;
|
|
58
|
+
credits(query?: CreditQuery): Promise<Page<CreditMovement>>;
|
|
59
|
+
allCredits(query?: CreditQuery): AsyncGenerator<CreditMovement, void, undefined>;
|
|
60
|
+
}
|