@fastermessage/sdk 0.1.0 → 0.1.2
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 +2 -2
- package/dist/cjs/client.js +21 -1
- package/dist/cjs/http.d.ts +1 -1
- package/dist/cjs/http.js +18 -4
- package/dist/cjs/resources.js +6 -1
- package/dist/esm/client.js +21 -1
- package/dist/esm/http.d.ts +1 -1
- package/dist/esm/http.js +18 -4
- package/dist/esm/resources.js +6 -1
- package/package.json +29 -7
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ const message = await fm.sms.send({
|
|
|
32
32
|
console.log(message.id, message.status); // queued
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Sur le domaine d'API de votre marque : `new FasterMessage({ ..., baseUrl: 'https://api.votre-marque.com' })`.
|
|
35
|
+
Sur le domaine d'API de votre marque : `new FasterMessage({ ..., baseUrl: 'https://api.votre-marque.com' })`. L'adresse doit être en `https` : `http` n'est accepté que vers la machine locale.
|
|
36
36
|
|
|
37
37
|
## Ce que fait le SDK
|
|
38
38
|
|
|
@@ -61,7 +61,7 @@ WhatsApp et la voix ne sont pas encore servis par l'API v2.
|
|
|
61
61
|
|
|
62
62
|
## Rejeux sans double envoi
|
|
63
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
|
|
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` (60 secondes au plus : au-delà, il lève `RateLimitError` avec `retryAfter`).
|
|
65
65
|
|
|
66
66
|
Passez votre propre clé pour survivre à un redémarrage de votre programme :
|
|
67
67
|
|
package/dist/cjs/client.js
CHANGED
|
@@ -4,6 +4,22 @@ exports.FasterMessage = exports.DEFAULT_BASE_URL = void 0;
|
|
|
4
4
|
const http_js_1 = require("./http.js");
|
|
5
5
|
const resources_js_1 = require("./resources.js");
|
|
6
6
|
exports.DEFAULT_BASE_URL = 'https://api.fastermessage.com';
|
|
7
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
8
|
+
/** Les identifiants ne partent qu'en https, sauf vers la machine locale. */
|
|
9
|
+
function checkedBaseUrl(value) {
|
|
10
|
+
const message = 'baseUrl doit être une adresse https, sans identifiants (http seulement vers la machine locale).';
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(value);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new TypeError(message);
|
|
17
|
+
}
|
|
18
|
+
const allowed = url.protocol === 'https:' || (url.protocol === 'http:' && LOCAL_HOSTS.has(url.hostname));
|
|
19
|
+
if (!allowed || !url.hostname || url.username || url.password || url.search || url.hash)
|
|
20
|
+
throw new TypeError(message);
|
|
21
|
+
return `${url.origin}${url.pathname}`.replace(/\/+$/, '');
|
|
22
|
+
}
|
|
7
23
|
/** Point d'entrée du SDK. Côté serveur seulement : il porte vos identifiants. */
|
|
8
24
|
class FasterMessage {
|
|
9
25
|
sms;
|
|
@@ -22,11 +38,15 @@ class FasterMessage {
|
|
|
22
38
|
if ('window' in globalThis && 'document' in globalThis) {
|
|
23
39
|
throw new Error('Le SDK FasterMessage ne s’utilise pas dans un navigateur : vos identifiants y seraient publics.');
|
|
24
40
|
}
|
|
41
|
+
if (!/^[\x21-\x7e]+$/.test(apiKey)) {
|
|
42
|
+
// Le message ne cite jamais la valeur : il finirait dans vos journaux.
|
|
43
|
+
throw new TypeError('La clé d’API contient des caractères invalides : vérifiez son copier-coller.');
|
|
44
|
+
}
|
|
25
45
|
this.#testMode = apiKey.includes('_test_');
|
|
26
46
|
const http = new http_js_1.Requester({
|
|
27
47
|
authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
|
|
28
48
|
apiKey,
|
|
29
|
-
baseUrl: (options.baseUrl ?? exports.DEFAULT_BASE_URL)
|
|
49
|
+
baseUrl: checkedBaseUrl(options.baseUrl ?? exports.DEFAULT_BASE_URL),
|
|
30
50
|
timeout: options.timeout ?? 30_000,
|
|
31
51
|
maxRetries: Math.max(0, options.maxRetries ?? 2),
|
|
32
52
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
package/dist/cjs/http.d.ts
CHANGED
package/dist/cjs/http.js
CHANGED
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Requester = exports.VERSION = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const errors_js_1 = require("./errors.js");
|
|
6
|
-
exports.VERSION = '0.1.
|
|
6
|
+
exports.VERSION = '0.1.2';
|
|
7
|
+
/** Au-delà, le SDK ne bloque pas votre programme : il rend la main avec RateLimitError. */
|
|
8
|
+
const MAX_RETRY_AFTER_SECONDS = 60;
|
|
7
9
|
/** Un même appel rejoué garde sa clé : c'est ce qui rend une reprise sans risque. */
|
|
8
10
|
const MUTATING = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
|
|
9
11
|
/** Le chemin unique de tous les appels : en-têtes, idempotence, reprises, erreurs. */
|
|
@@ -13,6 +15,8 @@ class Requester {
|
|
|
13
15
|
this.#config = config;
|
|
14
16
|
}
|
|
15
17
|
async request(method, path, { query, body, options = {} } = {}) {
|
|
18
|
+
checkHeader('idempotencyKey', options.idempotencyKey);
|
|
19
|
+
checkHeader('requestId', options.requestId);
|
|
16
20
|
const url = this.#config.baseUrl + path + queryString(query);
|
|
17
21
|
const headers = {
|
|
18
22
|
Authorization: this.#config.authorization,
|
|
@@ -41,6 +45,8 @@ class Requester {
|
|
|
41
45
|
method,
|
|
42
46
|
headers,
|
|
43
47
|
...(payload === undefined ? {} : { body: payload }),
|
|
48
|
+
// Une redirection recopierait la clé d'API vers l'adresse de destination.
|
|
49
|
+
redirect: 'manual',
|
|
44
50
|
signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
|
|
45
51
|
});
|
|
46
52
|
text = await response.text();
|
|
@@ -56,7 +62,7 @@ class Requester {
|
|
|
56
62
|
await sleep(backoff(attempt));
|
|
57
63
|
continue;
|
|
58
64
|
}
|
|
59
|
-
if (response.status <
|
|
65
|
+
if (response.status < 300)
|
|
60
66
|
return decode(response, text);
|
|
61
67
|
const failure = toError(response, text);
|
|
62
68
|
const wait = retryDelay(failure, attempt);
|
|
@@ -67,6 +73,11 @@ class Requester {
|
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
exports.Requester = Requester;
|
|
76
|
+
function checkHeader(name, value) {
|
|
77
|
+
if (value !== undefined && !/^[\x20-\x7e]*$/.test(value)) {
|
|
78
|
+
throw new TypeError(`${name} contient des caractères invalides.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
70
81
|
function queryString(query) {
|
|
71
82
|
if (!query)
|
|
72
83
|
return '';
|
|
@@ -84,8 +95,11 @@ function queryString(query) {
|
|
|
84
95
|
* ne servirait à rien. Le 501 dit une fonction non servie, jamais une panne.
|
|
85
96
|
*/
|
|
86
97
|
function retryDelay(failure, attempt) {
|
|
87
|
-
if (failure.status === 429)
|
|
88
|
-
|
|
98
|
+
if (failure.status === 429) {
|
|
99
|
+
if (failure.retryAfter === null || failure.retryAfter > MAX_RETRY_AFTER_SECONDS)
|
|
100
|
+
return null;
|
|
101
|
+
return failure.retryAfter * 1000;
|
|
102
|
+
}
|
|
89
103
|
if (failure.code === 'idempotency_key_in_flight')
|
|
90
104
|
return backoff(attempt);
|
|
91
105
|
if (failure.status >= 500 && failure.status !== 501)
|
package/dist/cjs/resources.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Account = exports.ContactGroups = exports.Contacts = exports.OtpResource = exports.Messages = exports.Email = exports.Sms = void 0;
|
|
4
|
-
|
|
4
|
+
function segment(id) {
|
|
5
|
+
// `..` changerait la route visée, et un identifiant vide viserait la collection.
|
|
6
|
+
if (id === '' || id === '.' || id === '..')
|
|
7
|
+
throw new TypeError('Identifiant de ressource invalide.');
|
|
8
|
+
return encodeURIComponent(id);
|
|
9
|
+
}
|
|
5
10
|
/** Parcourt toutes les pages, en demandant la suivante seulement quand il le faut. */
|
|
6
11
|
async function* paginate(http, path, query) {
|
|
7
12
|
let cursor = query.cursor;
|
package/dist/esm/client.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { Requester } from './http.js';
|
|
2
2
|
import { Account, ContactGroups, Contacts, Email, Messages, OtpResource, Sms } from './resources.js';
|
|
3
3
|
export const DEFAULT_BASE_URL = 'https://api.fastermessage.com';
|
|
4
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
5
|
+
/** Les identifiants ne partent qu'en https, sauf vers la machine locale. */
|
|
6
|
+
function checkedBaseUrl(value) {
|
|
7
|
+
const message = 'baseUrl doit être une adresse https, sans identifiants (http seulement vers la machine locale).';
|
|
8
|
+
let url;
|
|
9
|
+
try {
|
|
10
|
+
url = new URL(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new TypeError(message);
|
|
14
|
+
}
|
|
15
|
+
const allowed = url.protocol === 'https:' || (url.protocol === 'http:' && LOCAL_HOSTS.has(url.hostname));
|
|
16
|
+
if (!allowed || !url.hostname || url.username || url.password || url.search || url.hash)
|
|
17
|
+
throw new TypeError(message);
|
|
18
|
+
return `${url.origin}${url.pathname}`.replace(/\/+$/, '');
|
|
19
|
+
}
|
|
4
20
|
/** Point d'entrée du SDK. Côté serveur seulement : il porte vos identifiants. */
|
|
5
21
|
export class FasterMessage {
|
|
6
22
|
sms;
|
|
@@ -19,11 +35,15 @@ export class FasterMessage {
|
|
|
19
35
|
if ('window' in globalThis && 'document' in globalThis) {
|
|
20
36
|
throw new Error('Le SDK FasterMessage ne s’utilise pas dans un navigateur : vos identifiants y seraient publics.');
|
|
21
37
|
}
|
|
38
|
+
if (!/^[\x21-\x7e]+$/.test(apiKey)) {
|
|
39
|
+
// Le message ne cite jamais la valeur : il finirait dans vos journaux.
|
|
40
|
+
throw new TypeError('La clé d’API contient des caractères invalides : vérifiez son copier-coller.');
|
|
41
|
+
}
|
|
22
42
|
this.#testMode = apiKey.includes('_test_');
|
|
23
43
|
const http = new Requester({
|
|
24
44
|
authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
|
|
25
45
|
apiKey,
|
|
26
|
-
baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL)
|
|
46
|
+
baseUrl: checkedBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
|
|
27
47
|
timeout: options.timeout ?? 30_000,
|
|
28
48
|
maxRetries: Math.max(0, options.maxRetries ?? 2),
|
|
29
49
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
package/dist/esm/http.d.ts
CHANGED
package/dist/esm/http.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { ApiError, AuthenticationError, ConflictError, ConnectionError, InsufficientBalanceError, InvalidRequestError, NotFoundError, NotImplementedError, PermissionError, RateLimitError, ServerError, } from './errors.js';
|
|
3
|
-
export const VERSION = '0.1.
|
|
3
|
+
export const VERSION = '0.1.2';
|
|
4
|
+
/** Au-delà, le SDK ne bloque pas votre programme : il rend la main avec RateLimitError. */
|
|
5
|
+
const MAX_RETRY_AFTER_SECONDS = 60;
|
|
4
6
|
/** Un même appel rejoué garde sa clé : c'est ce qui rend une reprise sans risque. */
|
|
5
7
|
const MUTATING = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
|
|
6
8
|
/** Le chemin unique de tous les appels : en-têtes, idempotence, reprises, erreurs. */
|
|
@@ -10,6 +12,8 @@ export class Requester {
|
|
|
10
12
|
this.#config = config;
|
|
11
13
|
}
|
|
12
14
|
async request(method, path, { query, body, options = {} } = {}) {
|
|
15
|
+
checkHeader('idempotencyKey', options.idempotencyKey);
|
|
16
|
+
checkHeader('requestId', options.requestId);
|
|
13
17
|
const url = this.#config.baseUrl + path + queryString(query);
|
|
14
18
|
const headers = {
|
|
15
19
|
Authorization: this.#config.authorization,
|
|
@@ -38,6 +42,8 @@ export class Requester {
|
|
|
38
42
|
method,
|
|
39
43
|
headers,
|
|
40
44
|
...(payload === undefined ? {} : { body: payload }),
|
|
45
|
+
// Une redirection recopierait la clé d'API vers l'adresse de destination.
|
|
46
|
+
redirect: 'manual',
|
|
41
47
|
signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
|
|
42
48
|
});
|
|
43
49
|
text = await response.text();
|
|
@@ -53,7 +59,7 @@ export class Requester {
|
|
|
53
59
|
await sleep(backoff(attempt));
|
|
54
60
|
continue;
|
|
55
61
|
}
|
|
56
|
-
if (response.status <
|
|
62
|
+
if (response.status < 300)
|
|
57
63
|
return decode(response, text);
|
|
58
64
|
const failure = toError(response, text);
|
|
59
65
|
const wait = retryDelay(failure, attempt);
|
|
@@ -63,6 +69,11 @@ export class Requester {
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
}
|
|
72
|
+
function checkHeader(name, value) {
|
|
73
|
+
if (value !== undefined && !/^[\x20-\x7e]*$/.test(value)) {
|
|
74
|
+
throw new TypeError(`${name} contient des caractères invalides.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
66
77
|
function queryString(query) {
|
|
67
78
|
if (!query)
|
|
68
79
|
return '';
|
|
@@ -80,8 +91,11 @@ function queryString(query) {
|
|
|
80
91
|
* ne servirait à rien. Le 501 dit une fonction non servie, jamais une panne.
|
|
81
92
|
*/
|
|
82
93
|
function retryDelay(failure, attempt) {
|
|
83
|
-
if (failure.status === 429)
|
|
84
|
-
|
|
94
|
+
if (failure.status === 429) {
|
|
95
|
+
if (failure.retryAfter === null || failure.retryAfter > MAX_RETRY_AFTER_SECONDS)
|
|
96
|
+
return null;
|
|
97
|
+
return failure.retryAfter * 1000;
|
|
98
|
+
}
|
|
85
99
|
if (failure.code === 'idempotency_key_in_flight')
|
|
86
100
|
return backoff(attempt);
|
|
87
101
|
if (failure.status >= 500 && failure.status !== 501)
|
package/dist/esm/resources.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
function segment(id) {
|
|
2
|
+
// `..` changerait la route visée, et un identifiant vide viserait la collection.
|
|
3
|
+
if (id === '' || id === '.' || id === '..')
|
|
4
|
+
throw new TypeError('Identifiant de ressource invalide.');
|
|
5
|
+
return encodeURIComponent(id);
|
|
6
|
+
}
|
|
2
7
|
/** Parcourt toutes les pages, en demandant la suivante seulement quand il le faut. */
|
|
3
8
|
async function* paginate(http, path, query) {
|
|
4
9
|
let cursor = query.cursor;
|
package/package.json
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastermessage/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "SDK Node.js officiel de l'API FasterMessage v2 : SMS, e-mail, OTP, contacts et webhooks.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"fastermessage",
|
|
8
|
+
"sms",
|
|
9
|
+
"email",
|
|
10
|
+
"otp",
|
|
11
|
+
"api",
|
|
12
|
+
"sdk"
|
|
13
|
+
],
|
|
7
14
|
"homepage": "https://www.fastermessage.com/docs",
|
|
8
|
-
"repository": {
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/urbantech229/fastermessage-node.git"
|
|
18
|
+
},
|
|
9
19
|
"type": "module",
|
|
10
|
-
"engines": {
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.3"
|
|
22
|
+
},
|
|
11
23
|
"main": "./dist/cjs/index.js",
|
|
12
24
|
"module": "./dist/esm/index.js",
|
|
13
25
|
"types": "./dist/esm/index.d.ts",
|
|
14
26
|
"exports": {
|
|
15
27
|
".": {
|
|
16
|
-
"import": {
|
|
17
|
-
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/esm/index.d.ts",
|
|
30
|
+
"default": "./dist/esm/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/cjs/index.d.ts",
|
|
34
|
+
"default": "./dist/cjs/index.js"
|
|
35
|
+
}
|
|
18
36
|
}
|
|
19
37
|
},
|
|
20
|
-
"files": [
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
21
43
|
"sideEffects": false,
|
|
22
44
|
"scripts": {
|
|
23
45
|
"build": "node scripts/clean.mjs && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/cjs-package.mjs",
|