@fastermessage/sdk 0.0.0-stage → 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.
@@ -0,0 +1,30 @@
1
+ export declare const VERSION = "0.1.2";
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 {};
@@ -0,0 +1,166 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { ApiError, AuthenticationError, ConflictError, ConnectionError, InsufficientBalanceError, InvalidRequestError, NotFoundError, NotImplementedError, PermissionError, RateLimitError, ServerError, } from './errors.js';
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;
6
+ /** Un même appel rejoué garde sa clé : c'est ce qui rend une reprise sans risque. */
7
+ const MUTATING = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
8
+ /** Le chemin unique de tous les appels : en-têtes, idempotence, reprises, erreurs. */
9
+ export class Requester {
10
+ #config;
11
+ constructor(config) {
12
+ this.#config = config;
13
+ }
14
+ async request(method, path, { query, body, options = {} } = {}) {
15
+ checkHeader('idempotencyKey', options.idempotencyKey);
16
+ checkHeader('requestId', options.requestId);
17
+ const url = this.#config.baseUrl + path + queryString(query);
18
+ const headers = {
19
+ Authorization: this.#config.authorization,
20
+ 'X-Api-Key': this.#config.apiKey,
21
+ Accept: 'application/json',
22
+ 'User-Agent': `fastermessage-node/${VERSION}`,
23
+ };
24
+ if (body !== undefined)
25
+ headers['Content-Type'] = 'application/json';
26
+ if (options.requestId !== undefined)
27
+ headers['X-Request-Id'] = options.requestId;
28
+ let idempotencyKey = null;
29
+ if (MUTATING.has(method)) {
30
+ idempotencyKey = options.idempotencyKey ?? `sdk-${randomBytes(16).toString('hex')}`;
31
+ headers['Idempotency-Key'] = idempotencyKey;
32
+ }
33
+ const payload = body === undefined ? undefined : JSON.stringify(body);
34
+ const sleep = this.#config.sleep ?? defaultSleep;
35
+ for (let attempt = 0;; attempt++) {
36
+ let response;
37
+ let text;
38
+ try {
39
+ const timeout = AbortSignal.timeout(this.#config.timeout);
40
+ const fetchFn = this.#config.fetch ?? globalThis.fetch;
41
+ response = await fetchFn(url, {
42
+ method,
43
+ headers,
44
+ ...(payload === undefined ? {} : { body: payload }),
45
+ // Une redirection recopierait la clé d'API vers l'adresse de destination.
46
+ redirect: 'manual',
47
+ signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
48
+ });
49
+ text = await response.text();
50
+ }
51
+ catch (cause) {
52
+ if (options.signal?.aborted)
53
+ throw options.signal.reason;
54
+ if (attempt >= this.#config.maxRetries) {
55
+ const error = new ConnectionError(`Connexion à l'API impossible : ${describe(cause)}`, { cause });
56
+ error.idempotencyKey = idempotencyKey;
57
+ throw error;
58
+ }
59
+ await sleep(backoff(attempt));
60
+ continue;
61
+ }
62
+ if (response.status < 300)
63
+ return decode(response, text);
64
+ const failure = toError(response, text);
65
+ const wait = retryDelay(failure, attempt);
66
+ if (wait === null || attempt >= this.#config.maxRetries)
67
+ throw failure;
68
+ await sleep(wait);
69
+ }
70
+ }
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
+ }
77
+ function queryString(query) {
78
+ if (!query)
79
+ return '';
80
+ const params = new URLSearchParams();
81
+ for (const [key, value] of Object.entries(query)) {
82
+ if (value !== undefined && value !== null)
83
+ params.append(key, String(value));
84
+ }
85
+ const encoded = params.toString();
86
+ return encoded ? `?${encoded}` : '';
87
+ }
88
+ /**
89
+ * Reprise seulement si la même requête peut réussir plus tard. Un 429 sans
90
+ * `Retry-After` est le plafond OTP par numéro, à l'échelle de l'heure : le rejouer
91
+ * ne servirait à rien. Le 501 dit une fonction non servie, jamais une panne.
92
+ */
93
+ function retryDelay(failure, attempt) {
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
+ }
99
+ if (failure.code === 'idempotency_key_in_flight')
100
+ return backoff(attempt);
101
+ if (failure.status >= 500 && failure.status !== 501)
102
+ return backoff(attempt);
103
+ return null;
104
+ }
105
+ function backoff(attempt) {
106
+ return Math.min(8000, 500 * 2 ** attempt) * (1 + Math.random() * 0.25);
107
+ }
108
+ function defaultSleep(milliseconds) {
109
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
110
+ }
111
+ function describe(cause) {
112
+ return cause instanceof Error ? cause.message : String(cause);
113
+ }
114
+ function decode(response, text) {
115
+ if (response.status === 204 || text.trim() === '')
116
+ return undefined;
117
+ const data = parse(text);
118
+ if (typeof data !== 'object' || data === null) {
119
+ throw new ServerError("Réponse illisible de l'API.", {
120
+ status: response.status,
121
+ code: 'invalid_response',
122
+ type: 'api_error',
123
+ requestId: response.headers.get('x-request-id'),
124
+ });
125
+ }
126
+ return data;
127
+ }
128
+ function parse(text) {
129
+ try {
130
+ return JSON.parse(text);
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ function stringOrNull(value) {
137
+ return typeof value === 'string' ? value : null;
138
+ }
139
+ function toError(response, text) {
140
+ const decoded = parse(text);
141
+ const error = decoded && typeof decoded.error === 'object' && decoded.error !== null ? decoded.error : {};
142
+ const status = response.status;
143
+ const retryAfter = response.headers.get('retry-after');
144
+ const ErrorClass = errorClass(status);
145
+ return new ErrorClass(stringOrNull(error.message) ?? `Erreur HTTP ${status}.`, {
146
+ status,
147
+ code: stringOrNull(error.code) ?? `http_${status}`,
148
+ type: stringOrNull(error.type) ?? '',
149
+ param: stringOrNull(error.param),
150
+ requestId: stringOrNull(error.request_id) ?? response.headers.get('x-request-id'),
151
+ retryAfter: retryAfter !== null && /^\d+$/.test(retryAfter.trim()) ? Number(retryAfter) : null,
152
+ });
153
+ }
154
+ function errorClass(status) {
155
+ switch (status) {
156
+ case 400: return InvalidRequestError;
157
+ case 401: return AuthenticationError;
158
+ case 402: return InsufficientBalanceError;
159
+ case 403: return PermissionError;
160
+ case 404: return NotFoundError;
161
+ case 409: return ConflictError;
162
+ case 429: return RateLimitError;
163
+ case 501: return NotImplementedError;
164
+ default: return status >= 500 ? ServerError : ApiError;
165
+ }
166
+ }
@@ -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,4 @@
1
+ export { DEFAULT_BASE_URL, FasterMessage } from './client.js';
2
+ export * from './errors.js';
3
+ export { VERSION } from './http.js';
4
+ export { Webhook } from './webhook.js';
@@ -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
+ }
@@ -0,0 +1,126 @@
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
+ }
7
+ /** Parcourt toutes les pages, en demandant la suivante seulement quand il le faut. */
8
+ async function* paginate(http, path, query) {
9
+ let cursor = query.cursor;
10
+ for (;;) {
11
+ const page = await http.request('GET', path, { query: { ...query, cursor } });
12
+ yield* page.data ?? [];
13
+ cursor = page.next_cursor;
14
+ if (page.has_more !== true || cursor === null || cursor === undefined)
15
+ return;
16
+ }
17
+ }
18
+ export class Sms {
19
+ #http;
20
+ constructor(http) { this.#http = http; }
21
+ send(params, options) {
22
+ return this.#http.request('POST', '/v2/sms/send', { body: params, options });
23
+ }
24
+ /** Jusqu'à 500 lignes. Lisez toujours `errors` : un retour normal ne garantit pas que toutes soient parties. */
25
+ sendBatch(params, options) {
26
+ return this.#http.request('POST', '/v2/sms/batch', { body: params, options });
27
+ }
28
+ }
29
+ export class Email {
30
+ #http;
31
+ constructor(http) { this.#http = http; }
32
+ send(params, options) {
33
+ return this.#http.request('POST', '/v2/email/send', { body: params, options });
34
+ }
35
+ }
36
+ export class Messages {
37
+ #http;
38
+ constructor(http) { this.#http = http; }
39
+ /** Envoi avec le canal en paramètre. WhatsApp et la voix répondent aujourd'hui `501`. */
40
+ send(params, options) {
41
+ return this.#http.request('POST', '/v2/messages', { body: params, options });
42
+ }
43
+ get(id) {
44
+ return this.#http.request('GET', `/v2/messages/${segment(id)}`);
45
+ }
46
+ list(query = {}) {
47
+ return this.#http.request('GET', '/v2/messages', { query });
48
+ }
49
+ all(query = {}) {
50
+ return paginate(this.#http, '/v2/messages', query);
51
+ }
52
+ }
53
+ export class OtpResource {
54
+ #http;
55
+ constructor(http) { this.#http = http; }
56
+ request(params, options) {
57
+ return this.#http.request('POST', '/v2/otp', { body: { channel: 'sms', ...params }, options });
58
+ }
59
+ verify(id, code) {
60
+ return this.#http.request('POST', '/v2/otp/verify', { body: { id, code } });
61
+ }
62
+ }
63
+ export class Contacts {
64
+ #http;
65
+ constructor(http) { this.#http = http; }
66
+ create(params) {
67
+ return this.#http.request('POST', '/v2/contacts', { body: params });
68
+ }
69
+ /** Crée jusqu'à 500 contacts dans un groupe. */
70
+ createBatch(params, options) {
71
+ return this.#http.request('POST', '/v2/contacts/batch', { body: params, options });
72
+ }
73
+ get(id) {
74
+ return this.#http.request('GET', `/v2/contacts/${segment(id)}`);
75
+ }
76
+ list(query = {}) {
77
+ return this.#http.request('GET', '/v2/contacts', { query });
78
+ }
79
+ all(query = {}) {
80
+ return paginate(this.#http, '/v2/contacts', query);
81
+ }
82
+ update(id, params) {
83
+ return this.#http.request('PATCH', `/v2/contacts/${segment(id)}`, { body: params });
84
+ }
85
+ /** Suppression définitive. Ce n'est pas un désabonnement : pour cela, `consent: 'out'`. */
86
+ async delete(id) {
87
+ await this.#http.request('DELETE', `/v2/contacts/${segment(id)}`);
88
+ }
89
+ }
90
+ export class ContactGroups {
91
+ #http;
92
+ constructor(http) { this.#http = http; }
93
+ create(params) {
94
+ return this.#http.request('POST', '/v2/contact-groups', { body: params });
95
+ }
96
+ get(id) {
97
+ return this.#http.request('GET', `/v2/contact-groups/${segment(id)}`);
98
+ }
99
+ list(query = {}) {
100
+ return this.#http.request('GET', '/v2/contact-groups', { query });
101
+ }
102
+ all(query = {}) {
103
+ return paginate(this.#http, '/v2/contact-groups', query);
104
+ }
105
+ update(id, params) {
106
+ return this.#http.request('PATCH', `/v2/contact-groups/${segment(id)}`, { body: params });
107
+ }
108
+ /** Refusé en `409` tant que le groupe contient des contacts. */
109
+ async delete(id) {
110
+ await this.#http.request('DELETE', `/v2/contact-groups/${segment(id)}`);
111
+ }
112
+ }
113
+ export class Account {
114
+ #http;
115
+ constructor(http) { this.#http = http; }
116
+ async balance() {
117
+ const { balances } = await this.#http.request('GET', '/v2/balance');
118
+ return balances ?? [];
119
+ }
120
+ credits(query = {}) {
121
+ return this.#http.request('GET', '/v2/credits', { query });
122
+ }
123
+ allCredits(query = {}) {
124
+ return paginate(this.#http, '/v2/credits', query);
125
+ }
126
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Les réponses gardent les noms de champs de l'API. La signature ouverte de chaque
3
+ * ressource rend disponible un champ ajouté par l'API sans mise à jour du SDK.
4
+ */
5
+ export interface OpenRecord {
6
+ [field: string]: unknown;
7
+ }
8
+ export type Channel = 'sms' | 'email' | 'whatsapp' | 'voice';
9
+ export type Consent = 'in' | 'out' | (string & {});
10
+ export interface Page<T> extends OpenRecord {
11
+ data: T[];
12
+ has_more: boolean;
13
+ next_cursor: string | null;
14
+ }
15
+ export interface PageQuery {
16
+ limit?: number;
17
+ cursor?: string;
18
+ }
19
+ export interface MessageError extends OpenRecord {
20
+ code: string;
21
+ message: string;
22
+ provider_code: string | null;
23
+ }
24
+ export interface MessageRefund extends OpenRecord {
25
+ amount: number;
26
+ currency: string;
27
+ refunded_at: string;
28
+ }
29
+ export interface FallbackOutcome extends OpenRecord {
30
+ will_run: boolean;
31
+ reason: string | null;
32
+ message: string;
33
+ requested: Channel[];
34
+ }
35
+ export interface Message extends OpenRecord {
36
+ id: string;
37
+ channel: Channel;
38
+ status: string;
39
+ to: string;
40
+ from: string;
41
+ units: number;
42
+ price: number | null;
43
+ currency: string;
44
+ refund: MessageRefund | null;
45
+ client_reference: string | null;
46
+ fallback_of: string | null;
47
+ fallback?: FallbackOutcome | null;
48
+ error: MessageError | null;
49
+ created_at: string;
50
+ scheduled_at: string | null;
51
+ sent_at: string | null;
52
+ delivered_at: string | null;
53
+ }
54
+ interface SendCommon extends OpenRecord {
55
+ to: string;
56
+ fallback?: unknown[];
57
+ callback_url?: string;
58
+ scheduled_at?: string;
59
+ client_reference?: string;
60
+ }
61
+ export interface SendSmsParams extends SendCommon {
62
+ /** Sender ID approuvé : obligatoire, sans repli. */
63
+ from: string;
64
+ content: string | OpenRecord;
65
+ }
66
+ export interface EmailContent extends OpenRecord {
67
+ subject: string;
68
+ text?: string;
69
+ html?: string;
70
+ }
71
+ export interface SendEmailParams extends SendCommon {
72
+ from?: string;
73
+ content: EmailContent;
74
+ }
75
+ export interface SendMessageParams extends SendCommon {
76
+ channel: Channel;
77
+ from?: string;
78
+ content: unknown;
79
+ }
80
+ export interface SmsBatchLine extends OpenRecord {
81
+ to: string;
82
+ content?: string | OpenRecord;
83
+ from?: string;
84
+ client_reference?: string;
85
+ scheduled_at?: string;
86
+ callback_url?: string;
87
+ }
88
+ export interface SendSmsBatchParams extends OpenRecord {
89
+ /** De 1 à 500 lignes. */
90
+ messages: SmsBatchLine[];
91
+ from?: string;
92
+ content?: string | OpenRecord;
93
+ scheduled_at?: string;
94
+ callback_url?: string;
95
+ }
96
+ export interface BatchLineError extends OpenRecord {
97
+ index: number;
98
+ code: string;
99
+ message: string;
100
+ param?: string;
101
+ to?: string;
102
+ }
103
+ export interface SmsBatchResult extends OpenRecord {
104
+ total: number;
105
+ accepted: number;
106
+ rejected: number;
107
+ data: (Message & {
108
+ index: number;
109
+ })[];
110
+ /** Une entrée par ligne refusée : le reste du lot est bien parti. */
111
+ errors: BatchLineError[];
112
+ }
113
+ export interface MessageQuery extends PageQuery {
114
+ channel?: Channel;
115
+ status?: string;
116
+ [filter: string]: unknown;
117
+ }
118
+ export interface RequestOtpParams extends OpenRecord {
119
+ to: string;
120
+ /** `sms` par défaut. */
121
+ channel?: Channel;
122
+ from?: string;
123
+ length?: number;
124
+ ttl_seconds?: number;
125
+ template?: string;
126
+ client_reference?: string;
127
+ }
128
+ export interface Otp extends OpenRecord {
129
+ id: string;
130
+ status: string;
131
+ attempts_remaining: number;
132
+ }
133
+ export interface ContactFields extends OpenRecord {
134
+ email?: string;
135
+ whatsapp?: boolean;
136
+ consent?: Consent;
137
+ fields?: string[];
138
+ }
139
+ export interface CreateContactParams extends ContactFields {
140
+ group_id: string;
141
+ phone_number: string;
142
+ }
143
+ export interface UpdateContactParams extends OpenRecord {
144
+ group_id?: string;
145
+ phone_number?: string;
146
+ email?: string | null;
147
+ whatsapp?: boolean;
148
+ consent?: Consent;
149
+ fields?: string[];
150
+ }
151
+ export interface CreateContactBatchParams extends OpenRecord {
152
+ group_id: string;
153
+ /** De 1 à 500 lignes. */
154
+ contacts: (ContactFields & {
155
+ phone_number: string;
156
+ })[];
157
+ on_duplicate?: string;
158
+ }
159
+ export interface Contact extends OpenRecord {
160
+ id: string;
161
+ group_id: string;
162
+ phone_number: string;
163
+ consent: Consent;
164
+ }
165
+ export interface ContactBatchResult extends OpenRecord {
166
+ created: number;
167
+ invalid: number;
168
+ }
169
+ export interface ContactQuery extends PageQuery {
170
+ group_id?: string;
171
+ consent?: Consent;
172
+ search?: string;
173
+ }
174
+ export interface CreateContactGroupParams extends OpenRecord {
175
+ name: string;
176
+ fields?: string[];
177
+ }
178
+ export interface UpdateContactGroupParams extends OpenRecord {
179
+ name?: string;
180
+ fields?: string[];
181
+ }
182
+ export interface ContactGroup extends OpenRecord {
183
+ id: string;
184
+ name: string;
185
+ contacts_count: number;
186
+ fields: string[];
187
+ }
188
+ export interface Balance extends OpenRecord {
189
+ channel: Channel;
190
+ amount: number;
191
+ currency: string;
192
+ }
193
+ export interface CreditMovement extends OpenRecord {
194
+ id: string;
195
+ type: 'credit' | 'debit' | 'refund';
196
+ channel: Channel;
197
+ amount: number;
198
+ currency: string;
199
+ }
200
+ export interface CreditQuery extends PageQuery {
201
+ type?: 'credit' | 'debit' | 'refund';
202
+ [filter: string]: unknown;
203
+ }
204
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ /** Vérification des rappels reçus sur votre serveur. */
2
+ export declare const Webhook: {
3
+ /**
4
+ * Vrai si le rappel vient bien de la plateforme.
5
+ *
6
+ * @param rawBody le corps BRUT, avant tout décodage JSON
7
+ * @param signature l'en-tête `X-Fastermessage-Signature`
8
+ * @param secret le secret généré dans « Intégration & API »
9
+ */
10
+ isGenuine(rawBody: string | Uint8Array, signature: string | null | undefined, secret: string): boolean;
11
+ };
@@ -0,0 +1,18 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ /** Vérification des rappels reçus sur votre serveur. */
3
+ export const Webhook = {
4
+ /**
5
+ * Vrai si le rappel vient bien de la plateforme.
6
+ *
7
+ * @param rawBody le corps BRUT, avant tout décodage JSON
8
+ * @param signature l'en-tête `X-Fastermessage-Signature`
9
+ * @param secret le secret généré dans « Intégration & API »
10
+ */
11
+ isGenuine(rawBody, signature, secret) {
12
+ if (typeof signature !== 'string' || !secret)
13
+ return false;
14
+ const expected = Buffer.from(`sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`);
15
+ const received = Buffer.from(signature.trim());
16
+ return expected.length === received.length && timingSafeEqual(expected, received);
17
+ },
18
+ };