• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

univapay / univapay-node / 15916177987

27 Jun 2025 01:39AM UTC coverage: 98.635% (-0.1%) from 98.731%
15916177987

push

github

phieronymus
Bump to version 4.0.114

482 of 514 branches covered (93.77%)

Branch coverage included in aggregate %.

5590 of 5642 relevant lines covered (99.08%)

741.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.7
/src/api/RestAPI.ts
1
/**
3✔
2
 *  @module SDK/API
3✔
3
 */
3✔
4

3✔
5
import { stringify } from "@apimatic/json-bigint";
3✔
6
import { EventEmitter } from "events";
3✔
7
import pTimeout from "p-timeout";
3✔
8
import { stringify as stringifyQuery } from "query-string";
3✔
9
import flatten from "flat";
3✔
10

3✔
11
import {
3✔
12
    DEFAULT_ENDPOINT,
3✔
13
    ENV_KEY_APP_ID,
3✔
14
    ENV_KEY_APPLICATION_JWT,
3✔
15
    ENV_KEY_ENDPOINT,
3✔
16
    ENV_KEY_SECRET,
3✔
17
    IDEMPOTENCY_KEY_HEADER,
3✔
18
    POLLING_INTERVAL,
3✔
19
    POLLING_TIMEOUT,
3✔
20
    MAX_INTERNAL_ERROR_RETRY,
3✔
21
} from "../common/constants.js";
3✔
22
import { RequestErrorCode, ResponseErrorCode } from "../errors/APIError.js";
3✔
23
import { fromError } from "../errors/parser.js";
3✔
24
import { ResponseError } from "../errors/RequestResponseError.js";
3✔
25
import { TimeoutError } from "../errors/TimeoutError.js";
3✔
26
import { ProcessingMode } from "../resources/common/enums.js";
3✔
27
import { checkStatus, parseJSON } from "../utils/fetch.js";
3✔
28
import { isBlob, toSnakeCase, transformKeys } from "../utils/object.js";
3✔
29

3✔
30
import { extractJWT, JWTPayload, parseJWT } from "./utils/JWT.js";
3✔
31
import { containsBinaryData, objectToFormData } from "./utils/payload.js";
3✔
32
import { userAgent } from "./utils/userAgent.js";
3✔
33

3✔
34
export type PollParams<Response> = {
3✔
35
    /**
3✔
36
     * Condition for which the response is considered to be successful and stop the polling
3✔
37
     */
3✔
38
    successCondition: (response: Response) => boolean;
3✔
39

3✔
40
    /**
3✔
41
     * Condition to cancel the polling without triggering error
3✔
42
     */
3✔
43
    cancelCondition?: (response: Response) => boolean;
3✔
44

3✔
45
    /**
3✔
46
     * Time after which a TimeoutError will be triggered and the poll will be canceled
3✔
47
     */
3✔
48
    timeout?: number;
3✔
49

3✔
50
    /**
3✔
51
     * Interval between polls or function to compute the interval
3✔
52
     */
3✔
53
    interval?: number | (() => number);
3✔
54

3✔
55
    /**
3✔
56
     * Callback to be triggered at each poll iteration
3✔
57
     */
3✔
58
    iterationCallback?: (response: Response) => void;
3✔
59

3✔
60
    /**
3✔
61
     * Boolean when the call should not be executed when the tab is inactive. Defaults to false.
3✔
62
     * Only works on a valid webbrowser environment
3✔
63
     */
3✔
64
    browserSkipCallForInactiveTabs?: boolean;
3✔
65
};
3✔
66

3✔
67
export type BodyTransferType = "entity" | "message";
3✔
68

3✔
69
export enum HTTPMethod {
3✔
70
    GET = "GET",
3✔
71
    POST = "POST",
3✔
72
    PATCH = "PATCH",
3✔
73
    PUT = "PUT",
3✔
74
    DELETE = "DELETE",
3✔
75
    OPTION = "OPTION",
3✔
76
    HEAD = "HEAD",
3✔
77
}
3✔
78

3✔
79
export interface RestAPIOptions {
3✔
80
    endpoint?: string;
3✔
81
    jwt?: string;
3✔
82
    handleUpdateJWT?: (jwt: string) => void;
3✔
83
    secret?: string;
3✔
84
    origin?: string;
3✔
85
    authParams?: DefaultAuthParams;
3✔
86

3✔
87
    // Deprecated
3✔
88
    authToken?: string;
3✔
89
    appId?: string;
3✔
90
}
3✔
91

3✔
92
export interface ApplicationToken {
3✔
93
    sub: "app_token";
3✔
94
}
3✔
95

3✔
96
export interface StoreToken extends ApplicationToken {
3✔
97
    storeId: string;
3✔
98
    mode: ProcessingMode;
3✔
99
    domains: string[];
3✔
100
}
3✔
101

3✔
102
export interface SubError {
3✔
103
    reason: RequestErrorCode | ResponseErrorCode;
3✔
104
    rawError?: boolean | number | string | SubError | ValidationError;
3✔
105
}
3✔
106

3✔
107
export interface ValidationError extends SubError {
3✔
108
    field: string;
3✔
109
}
3✔
110

3✔
111
export interface ErrorResponse {
3✔
112
    httpCode?: number;
3✔
113
    status: string;
3✔
114
    code: ResponseErrorCode | RequestErrorCode;
3✔
115
    errors: (SubError | ValidationError)[];
3✔
116
}
3✔
117

3✔
118
export interface AuthParams {
3✔
119
    jwt?: string;
3✔
120
    secret?: string;
3✔
121
    idempotentKey?: string;
3✔
122
    origin?: string;
3✔
123
    useCredentials?: boolean;
3✔
124

3✔
125
    // Deprecated
3✔
126
    authToken?: string;
3✔
127
    appId?: string;
3✔
128
}
3✔
129

3✔
130
export type DefaultAuthParams = {
3✔
131
    useCredentials?: boolean;
3✔
132
};
3✔
133

3✔
134
export type PollData = {
3✔
135
    polling?: boolean;
3✔
136
};
3✔
137

3✔
138
export type PollExecute<A> = () => Promise<A>;
3✔
139

3✔
140
export type SendData<Data> = Data;
3✔
141

3✔
142
export type ApiSendOptions = {
3✔
143
    /**
3✔
144
     * Validate the JWT before calling the route and adds the authorization header to the request.
3✔
145
     */
3✔
146
    requireAuth?: boolean;
3✔
147

3✔
148
    /**
3✔
149
     * Add the custom type to the header. Defaults to `application/json`
3✔
150
     */
3✔
151
    acceptType?: string;
3✔
152

3✔
153
    /**
3✔
154
     * Custom formatter to format the request object to the API (skipped for blobs)
3✔
155
     */
3✔
156
    keyFormatter?: (key: string) => string;
3✔
157

3✔
158
    /**
3✔
159
     * List of key to ignore formatting on when parsing the response
3✔
160
     */
3✔
161
    ignoreKeysFormatting?: string[];
3✔
162

3✔
163
    /**
3✔
164
     * Overrides default body transfer encoding of the request.
3✔
165
     *
3✔
166
     * Note: Can not for GET and HEAD request
3✔
167
     *
3✔
168
     * Default is "message-body" for DELETE, HEAD and GET and "entity-body" for all other call methods.
3✔
169
     */
3✔
170
    bodyTransferEncoding?: BodyTransferType;
3✔
171

3✔
172
    /**
3✔
173
     * Some of the services require query impersonate to be present.
3✔
174
     */
3✔
175
    queryImpersonate?: string;
3✔
176

3✔
177
    requestInit?: RequestInit;
3✔
178
};
3✔
179

3✔
180
const getRequestBody = <Data>(
3✔
181
    data: SendData<Data>,
138✔
182
    keyFormatter = toSnakeCase,
138✔
183
    ignoreKeysFormatting: string[],
138✔
184
): string | FormData | Blob =>
138✔
185
    isBlob(data)
138✔
186
        ? data
138!
187
        : containsBinaryData(data)
138✔
188
          ? objectToFormData(data, keyFormatter, ignoreKeysFormatting)
138!
189
          : stringify(transformKeys(data, keyFormatter, ignoreKeysFormatting));
3✔
190

3✔
191
const stringifyParams = (data: unknown): string => {
3✔
192
    const query = stringifyQuery(flatten(transformKeys(data, toSnakeCase), { safe: true }), {
9,700✔
193
        arrayFormat: "bracket",
9,700✔
194
    });
9,700✔
195

9,700✔
196
    return query ? `?${query}` : "";
9,700✔
197
};
9,700✔
198

3✔
199
const execRequest = async <Response>(execute: () => Promise<Response>): Promise<Response> => {
3✔
200
    try {
9,817✔
201
        const response = await execute();
9,817✔
202

9,673✔
203
        return response;
9,673✔
204
    } catch (error) {
9,817✔
205
        const formattedError =
144✔
206
            error instanceof TimeoutError || error instanceof ResponseError ? error : fromError(error);
144✔
207

144✔
208
        throw formattedError;
144✔
209
    }
144✔
210
};
9,817✔
211

3✔
212
export class RestAPI extends EventEmitter {
3✔
213
    endpoint: string;
3✔
214
    jwt: JWTPayload<unknown>;
3✔
215
    origin: string;
3✔
216
    secret: string;
3✔
217

3✔
218
    defaultAuthParams?: DefaultAuthParams;
3✔
219

3✔
220
    /**
3✔
221
     *  @deprecated
3✔
222
     */
3✔
223
    appId: string;
3✔
224

3✔
225
    /**
3✔
226
     *  @deprecated
3✔
227
     */
3✔
228
    authToken: string;
3✔
229

3✔
230
    private _jwtRaw: string = null;
3✔
231

3✔
232
    protected handleUpdateJWT: (jwt: string) => void;
3✔
233

3✔
234
    constructor(options: RestAPIOptions = {}) {
3✔
235
        super();
557✔
236

557✔
237
        this.endpoint = options.endpoint || process.env[ENV_KEY_ENDPOINT] || DEFAULT_ENDPOINT;
557!
238
        this.origin = options.origin || this.origin;
557✔
239
        this.jwtRaw = options.jwt || process.env[ENV_KEY_APPLICATION_JWT];
557✔
240
        this.handleUpdateJWT = options.handleUpdateJWT || undefined;
557✔
241
        this.defaultAuthParams = options.authParams;
557✔
242

557✔
243
        this.appId = options.appId || process.env[ENV_KEY_APP_ID];
557✔
244
        this.secret = options.secret || process.env[ENV_KEY_SECRET];
557✔
245
        this.authToken = options.authToken;
557✔
246
    }
557✔
247

3✔
248
    set jwtRaw(jwtRaw: string) {
3✔
249
        this._jwtRaw = jwtRaw;
575✔
250
        this.jwt = parseJWT(jwtRaw);
575✔
251
    }
575✔
252

3✔
253
    get jwtRaw(): string | null {
3✔
254
        return this._jwtRaw;
9,721✔
255
    }
9,721✔
256

3✔
257
    /**
3✔
258
     * @internal
3✔
259
     */
3✔
260
    async send<ResponseBody, Data = unknown>(
3✔
261
        method: HTTPMethod,
9,700✔
262
        uri: string,
9,700✔
263
        data?: SendData<Data>,
9,700✔
264
        auth?: AuthParams,
9,700✔
265
        options: ApiSendOptions = {},
9,700✔
266
    ): Promise<ResponseBody | string | Blob | FormData> {
9,700✔
267
        const authParams = { ...this.defaultAuthParams, ...auth };
9,700✔
268
        const {
9,700✔
269
            acceptType,
9,700✔
270
            keyFormatter = toSnakeCase,
9,700✔
271
            ignoreKeysFormatting = ["metadata"],
9,700✔
272
            bodyTransferEncoding,
9,700✔
273
            queryImpersonate,
9,700✔
274
            requestInit,
9,700✔
275
        } = options;
9,700✔
276

9,700✔
277
        const payload: boolean =
9,700✔
278
            bodyTransferEncoding === "entity"
9,700✔
279
                ? ![HTTPMethod.GET, HTTPMethod.HEAD].includes(method)
9,700✔
280
                : bodyTransferEncoding === "message"
9,700✔
281
                  ? false
9,679✔
282
                  : ![HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.DELETE].includes(method);
9,700✔
283

9,700✔
284
        const params: RequestInit = {
9,700✔
285
            headers: this.getHeaders(data, authParams, payload, acceptType),
9,700✔
286
            method,
9,700✔
287
            ...(authParams?.useCredentials ? { credentials: "include" } : {}),
9,700✔
288
        };
9,700✔
289

9,700✔
290
        const query = stringifyParams({
9,700✔
291
            ...(payload || !data ? {} : data),
9,700✔
292
            ...(queryImpersonate ? { impersonate: queryImpersonate } : {}),
9,700✔
293
        });
9,700✔
294
        const request: Request = new Request(
9,700✔
295
            `${/^https?:\/\//.test(uri) ? uri : `${this.endpoint}${uri}`}${query}`,
9,700✔
296
            payload ? { ...params, body: getRequestBody(data, keyFormatter, ignoreKeysFormatting) } : params,
9,700✔
297
        );
9,700✔
298

9,700✔
299
        this.emit("request", request);
9,700✔
300

9,700✔
301
        return execRequest<ResponseBody | string | Blob | FormData>(async () => {
9,700✔
302
            const response = await fetch(request, requestInit);
9,700✔
303

9,700✔
304
            this.emit("response", response);
9,700✔
305

9,700✔
306
            const jwt = extractJWT(response);
9,700✔
307

9,700✔
308
            if (jwt) {
9,700✔
309
                this.jwtRaw = jwt;
18✔
310
                this.handleUpdateJWT?.(jwt);
18✔
311
            }
18✔
312

9,700✔
313
            await checkStatus(response);
9,700✔
314

9,601✔
315
            const noContentStatus = 204;
9,601✔
316
            if (response.status === noContentStatus) {
9,700✔
317
                return "";
21✔
318
            }
21✔
319

9,580✔
320
            if (response.status === 303 && options.requestInit.redirect === "manual") {
9,700!
321
                const redirect = response.headers.get("location");
×
322
                return this.send(method, redirect, data, auth, options);
×
323
            }
×
324

9,580✔
325
            const contentType = response.headers.get("content-type");
9,580✔
326
            if (contentType) {
9,700✔
327
                if (contentType.indexOf("application/json") !== -1) {
9,577✔
328
                    return parseJSON<ResponseBody>(response, ignoreKeysFormatting);
9,568✔
329
                } else if (contentType.indexOf("text/") === 0) {
9,577✔
330
                    return response.text();
3✔
331
                } else if (contentType.indexOf("multipart/") === 0) {
9!
332
                    return response.formData();
×
333
                }
×
334
            }
9,577✔
335

9✔
336
            return response.blob();
9✔
337
        });
9,700✔
338
    }
9,700✔
339

3✔
340
    protected getHeaders<Data = unknown>(
3✔
341
        data: SendData<Data>,
9,700✔
342
        auth: AuthParams,
9,700✔
343
        payload: boolean,
9,700✔
344
        acceptType = "application/json",
9,700✔
345
    ): Headers {
9,700✔
346
        const headers: Headers = new Headers();
9,700✔
347
        const isFormData = containsBinaryData(data);
9,700✔
348

9,700✔
349
        headers.append("Accept", acceptType);
9,700✔
350

9,700✔
351
        if (!isFormData && payload) {
9,700✔
352
            headers.append("Content-Type", "application/json");
138✔
353
        }
138✔
354

9,700✔
355
        const {
9,700✔
356
            origin = this.origin,
9,700✔
357
            idempotentKey = null,
9,700✔
358
            authToken = this.authToken,
9,700✔
359
            appId = this.appId,
9,700✔
360
            secret = this.secret,
9,700✔
361
            jwt = this.jwtRaw,
9,700✔
362
        } = auth || {};
9,700!
363

9,700✔
364
        headers.append("User-Agent", userAgent());
9,700✔
365

9,700✔
366
        if (origin) {
9,700✔
367
            headers.append("Origin", origin);
9✔
368
        }
9✔
369

9,700✔
370
        if (idempotentKey) {
9,700✔
371
            headers.append(IDEMPOTENCY_KEY_HEADER, idempotentKey);
3✔
372
        }
3✔
373

9,700✔
374
        if (authToken) {
9,700✔
375
            headers.append("Authorization", `Token ${authToken}`);
9✔
376
        } else if (appId) {
9,700✔
377
            headers.append("Authorization", `ApplicationToken ${appId}|${secret || ""}`);
18✔
378
        } else if (jwt) {
9,691✔
379
            if (secret) {
24✔
380
                headers.append("Authorization", `Bearer ${secret}.${jwt}`);
6✔
381
            } else {
24✔
382
                headers.append("Authorization", `Bearer ${jwt}`);
18✔
383
            }
18✔
384
        }
24✔
385

9,700✔
386
        return headers;
9,700✔
387
    }
9,700✔
388

3✔
389
    /**
3✔
390
     * @internal
3✔
391
     */
3✔
392
    async longPolling<Response>(execute: PollExecute<Response>, pollParams: PollParams<Response>): Promise<Response> {
3✔
393
        const {
117✔
394
            successCondition,
117✔
395
            cancelCondition,
117✔
396
            iterationCallback,
117✔
397
            timeout = POLLING_TIMEOUT,
117✔
398
            interval = POLLING_INTERVAL,
117✔
399
            browserSkipCallForInactiveTabs = false,
117✔
400
        } = pollParams;
117✔
401

117✔
402
        return execRequest(async () => {
117✔
403
            let internalErrorCount = 0;
117✔
404

117✔
405
            const computedInterval = typeof interval === "number" ? interval : interval();
117!
406
            const sleepInterval = () => new Promise((resolve) => setTimeout(resolve, computedInterval));
117✔
407

117✔
408
            const repeater = async (): Promise<Response> => {
117✔
409
                if (browserSkipCallForInactiveTabs && document?.hidden) {
9,234!
410
                    await sleepInterval();
×
411
                    return repeater();
×
412
                }
×
413

9,234✔
414
                try {
9,234✔
415
                    const result = await execute();
9,234✔
416

9,144✔
417
                    iterationCallback?.(result);
9,234!
418

9,234✔
419
                    if (cancelCondition?.(result)) {
9,234✔
420
                        return null;
12✔
421
                    }
12✔
422

9,132✔
423
                    if (!successCondition(result)) {
9,234✔
424
                        await sleepInterval();
9,072✔
425
                        return repeater();
9,057✔
426
                    }
9,057✔
427

60✔
428
                    return result;
60✔
429
                } catch (error) {
9,234✔
430
                    // Use retry mechanism for 500 as internal server error on API side do not always mean failure
90✔
431
                    if (error.errorResponse?.httpCode !== 500 || internalErrorCount >= MAX_INTERNAL_ERROR_RETRY) {
90✔
432
                        throw error;
30✔
433
                    }
30✔
434

60✔
435
                    internalErrorCount++;
60✔
436
                    await sleepInterval();
60✔
437
                    return repeater();
60✔
438
                }
60✔
439
            };
117✔
440

117✔
441
            return pTimeout(repeater(), timeout, new TimeoutError(timeout));
117✔
442
        });
117✔
443
    }
117✔
444

3✔
445
    async ping(): Promise<void> {
3✔
446
        await this.send(HTTPMethod.GET, "/heartbeat", null, null, { requireAuth: false });
6✔
447
    }
6✔
448
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc