@tapsilat/tapsilat-js 2026.10.20 → 2026.10.26
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/CHANGELOG.md +9 -0
- package/README.md +99 -26
- package/dist/TapsilatSDK.d.ts +58 -9
- package/dist/TapsilatSDK.d.ts.map +1 -1
- package/dist/errors/TapsilatError.d.ts +2 -0
- package/dist/errors/TapsilatError.d.ts.map +1 -1
- package/dist/http/HttpClient.d.ts +9 -0
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/interceptors.d.ts.map +1 -1
- package/dist/index.cjs +564 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +385 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +557 -42
- package/dist/index.js.map +1 -1
- package/dist/types/elements.d.ts +144 -0
- package/dist/types/elements.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -13
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/redact.d.ts +16 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/verify.d.ts +31 -8
- package/dist/utils/verify.d.ts.map +1 -1
- package/dist/webhooks/OrderStatus.d.ts +98 -0
- package/dist/webhooks/OrderStatus.d.ts.map +1 -0
- package/dist/webhooks/WebhookEvent.d.ts +58 -0
- package/dist/webhooks/WebhookEvent.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ class TapsilatError extends Error {
|
|
|
18
18
|
apiStatus;
|
|
19
19
|
/** Per-field validation messages from the API response body */
|
|
20
20
|
errors;
|
|
21
|
+
/** Whether the API error says the request can be retried, as Elements errors do */
|
|
22
|
+
retryable;
|
|
21
23
|
/** Raw API response body */
|
|
22
24
|
rawBody;
|
|
23
25
|
/**
|
|
@@ -53,6 +55,7 @@ class TapsilatError extends Error {
|
|
|
53
55
|
error.apiCode = apiError.apiCode;
|
|
54
56
|
error.apiStatus = apiError.apiStatus;
|
|
55
57
|
error.errors = apiError.errors;
|
|
58
|
+
error.retryable = apiError.retryable;
|
|
56
59
|
error.rawBody = apiError.rawBody;
|
|
57
60
|
return error;
|
|
58
61
|
}
|
|
@@ -519,6 +522,8 @@ class HttpClient {
|
|
|
519
522
|
const errors = body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)
|
|
520
523
|
? body.errors
|
|
521
524
|
: undefined;
|
|
525
|
+
const nested = this.nestedError(body);
|
|
526
|
+
const apiCode = body.code !== undefined && body.code !== null ? body.code : nested?.code;
|
|
522
527
|
return {
|
|
523
528
|
success: false,
|
|
524
529
|
status: response.status,
|
|
@@ -527,13 +532,14 @@ class HttpClient {
|
|
|
527
532
|
code: error.code,
|
|
528
533
|
details: error.details,
|
|
529
534
|
statusCode: response.status,
|
|
530
|
-
apiCode:
|
|
531
|
-
? String(
|
|
535
|
+
apiCode: apiCode !== undefined && apiCode !== null
|
|
536
|
+
? String(apiCode)
|
|
532
537
|
: undefined,
|
|
533
538
|
apiStatus: body.status !== undefined && body.status !== null
|
|
534
539
|
? String(body.status)
|
|
535
540
|
: undefined,
|
|
536
541
|
errors,
|
|
542
|
+
retryable: typeof nested?.retryable === "boolean" ? nested.retryable : undefined,
|
|
537
543
|
rawBody,
|
|
538
544
|
},
|
|
539
545
|
};
|
|
@@ -591,10 +597,28 @@ class HttpClient {
|
|
|
591
597
|
}
|
|
592
598
|
if (data && typeof data === "object") {
|
|
593
599
|
const obj = data;
|
|
594
|
-
|
|
600
|
+
const nested = this.nestedError(obj);
|
|
601
|
+
return (obj.message || (nested ? nested.message : obj.error) || obj.detail);
|
|
595
602
|
}
|
|
596
603
|
return undefined;
|
|
597
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* @category HTTP Client Internals
|
|
607
|
+
* @summary Returns the error object of a nested error body
|
|
608
|
+
* @description Elements routes answer with {"error": {"code", "message", "retryable"}} instead of top-level fields
|
|
609
|
+
*
|
|
610
|
+
* @param data - The parsed response data
|
|
611
|
+
* @returns The nested error object, or undefined when `error` is not an object
|
|
612
|
+
*/
|
|
613
|
+
nestedError(data) {
|
|
614
|
+
if (!data || typeof data !== "object") {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
const error = data.error;
|
|
618
|
+
return error && typeof error === "object" && !Array.isArray(error)
|
|
619
|
+
? error
|
|
620
|
+
: undefined;
|
|
621
|
+
}
|
|
598
622
|
/**
|
|
599
623
|
* @category HTTP Client Internals
|
|
600
624
|
* @summary Extracts validation details from error response
|
|
@@ -1881,30 +1905,129 @@ class CurrencyResolver {
|
|
|
1881
1905
|
}
|
|
1882
1906
|
}
|
|
1883
1907
|
|
|
1908
|
+
/** What a redacted secret prints as */
|
|
1909
|
+
const REDACTED = "[redacted]";
|
|
1910
|
+
/** The hook Node.js calls when console.log or util.inspect prints an object */
|
|
1911
|
+
const INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
1912
|
+
/**
|
|
1913
|
+
* Makes console.log and util.inspect print an object's `client_secret` as "[redacted]"
|
|
1914
|
+
*
|
|
1915
|
+
* @summary Keep a client secret out of logs
|
|
1916
|
+
* @description
|
|
1917
|
+
* Adds a non-enumerable inspect hook to the object itself, so JSON.stringify, property access and
|
|
1918
|
+
* Object.keys still see the real secret. A copy made with spread or structuredClone does not keep
|
|
1919
|
+
* the hook.
|
|
1920
|
+
*
|
|
1921
|
+
* @param value - A response object that carries `client_secret`
|
|
1922
|
+
* @returns The same object
|
|
1923
|
+
*/
|
|
1924
|
+
const redactClientSecret = (value) => {
|
|
1925
|
+
if (value && typeof value === "object" && "client_secret" in value) {
|
|
1926
|
+
Object.defineProperty(value, INSPECT, {
|
|
1927
|
+
value: function () {
|
|
1928
|
+
return { ...this, client_secret: REDACTED };
|
|
1929
|
+
},
|
|
1930
|
+
enumerable: false,
|
|
1931
|
+
configurable: true,
|
|
1932
|
+
writable: true,
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
return value;
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
/** The header that carries the signature of every callback Tapsilat sends */
|
|
1939
|
+
const WEBHOOK_SIGNATURE_HEADER = "Tapsilat-Signature";
|
|
1940
|
+
/** How many seconds a callback's signature timestamp may differ from the current time */
|
|
1941
|
+
const WEBHOOK_TOLERANCE = 300;
|
|
1942
|
+
const LEGACY_PREFIX = "sha256=";
|
|
1943
|
+
const hmacHex = (secret, ...parts) => {
|
|
1944
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
1945
|
+
for (const part of parts) {
|
|
1946
|
+
hmac.update(part);
|
|
1947
|
+
}
|
|
1948
|
+
return hmac.digest("hex");
|
|
1949
|
+
};
|
|
1950
|
+
// Compare in constant time so the signature cannot be guessed byte by byte
|
|
1951
|
+
const safeEqual = (expected, candidate) => {
|
|
1952
|
+
const a = Buffer.from(expected);
|
|
1953
|
+
const b = Buffer.from(candidate);
|
|
1954
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
1955
|
+
};
|
|
1956
|
+
const checkSeconds = (value, name) => {
|
|
1957
|
+
if (value !== undefined && (typeof value !== "number" || Number.isNaN(value))) {
|
|
1958
|
+
throw new TapsilatValidationError(`Webhook ${name} must be a number of seconds`);
|
|
1959
|
+
}
|
|
1960
|
+
return value;
|
|
1961
|
+
};
|
|
1884
1962
|
// WEBHOOK SIGNATURE VERIFICATION
|
|
1885
|
-
// Summary:
|
|
1886
|
-
// Description:
|
|
1963
|
+
// Summary: Verify the Tapsilat-Signature header of a callback
|
|
1964
|
+
// Description: Checks the timestamped HMAC-SHA256 that Tapsilat signs every callback with
|
|
1887
1965
|
/**
|
|
1888
|
-
* Verifies
|
|
1966
|
+
* Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
|
|
1889
1967
|
*
|
|
1890
|
-
* @summary
|
|
1891
|
-
* @description
|
|
1968
|
+
* @summary Verify the Tapsilat-Signature header of a callback
|
|
1969
|
+
* @description
|
|
1970
|
+
* Tapsilat sends "t=<unix timestamp>,v1=<hex HMAC-SHA256 of '<timestamp>.<raw body>'>". The timestamp
|
|
1971
|
+
* must be within `tolerance` seconds of `now` so that a captured callback cannot be replayed. The
|
|
1972
|
+
* comparison takes constant time.
|
|
1973
|
+
*
|
|
1974
|
+
* A bare hex HMAC-SHA256 of the payload, with or without a "sha256=" prefix, is still accepted for
|
|
1975
|
+
* signatures made the way earlier versions of this SDK documented.
|
|
1892
1976
|
*
|
|
1893
|
-
* @param payload -
|
|
1894
|
-
* @param signature -
|
|
1895
|
-
* @param secret -
|
|
1896
|
-
* @
|
|
1977
|
+
* @param payload - The raw request body, before any JSON decoding
|
|
1978
|
+
* @param signature - The Tapsilat-Signature header
|
|
1979
|
+
* @param secret - The webhook secret from the panel's callback settings
|
|
1980
|
+
* @param options - Timestamp tolerance and the current time
|
|
1981
|
+
* @returns true when the signature is valid; false when it is missing, malformed, stale or wrong
|
|
1982
|
+
* @throws {TapsilatValidationError} When payload is not a string or bytes, or an option is not a number
|
|
1897
1983
|
*/
|
|
1898
|
-
const
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1984
|
+
const verifyWebhookSignature = (payload, signature, secret, options = {}) => {
|
|
1985
|
+
if (typeof payload !== "string" && !(payload instanceof Uint8Array)) {
|
|
1986
|
+
throw new TapsilatValidationError("Webhook payload must be the raw request body as a string or Buffer");
|
|
1987
|
+
}
|
|
1988
|
+
const tolerance = checkSeconds(options.tolerance, "tolerance") ?? WEBHOOK_TOLERANCE;
|
|
1989
|
+
const now = checkSeconds(options.now, "now") ?? Math.floor(Date.now() / 1000);
|
|
1990
|
+
const header = typeof signature === "string" ? signature.trim() : "";
|
|
1991
|
+
if (header === "" || typeof secret !== "string" || secret === "") {
|
|
1992
|
+
return false;
|
|
1993
|
+
}
|
|
1994
|
+
if (header.startsWith(LEGACY_PREFIX) || !header.includes("=")) {
|
|
1995
|
+
const legacy = header.startsWith(LEGACY_PREFIX) ? header.slice(LEGACY_PREFIX.length) : header;
|
|
1996
|
+
return safeEqual(hmacHex(secret, payload), legacy.toLowerCase());
|
|
1997
|
+
}
|
|
1998
|
+
let timestamp;
|
|
1999
|
+
const candidates = [];
|
|
2000
|
+
for (const part of header.split(",")) {
|
|
2001
|
+
const pair = part.trim();
|
|
2002
|
+
const separator = pair.indexOf("=");
|
|
2003
|
+
if (separator === -1) {
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const key = pair.slice(0, separator);
|
|
2007
|
+
const value = pair.slice(separator + 1);
|
|
2008
|
+
if (key === "t") {
|
|
2009
|
+
timestamp = value;
|
|
2010
|
+
}
|
|
2011
|
+
else if (key === "v1") {
|
|
2012
|
+
candidates.push(value.toLowerCase());
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (timestamp === undefined || !/^[0-9]+$/.test(timestamp) || candidates.length === 0) {
|
|
2016
|
+
return false;
|
|
2017
|
+
}
|
|
2018
|
+
const signedAt = Number(timestamp);
|
|
2019
|
+
if (!Number.isSafeInteger(signedAt)) {
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
if (tolerance > 0 && Math.abs(now - signedAt) > tolerance) {
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
const expected = hmacHex(secret, `${timestamp}.`, payload);
|
|
2026
|
+
let valid = false;
|
|
2027
|
+
for (const candidate of candidates) {
|
|
2028
|
+
valid = safeEqual(expected, candidate) || valid;
|
|
2029
|
+
}
|
|
2030
|
+
return valid;
|
|
1908
2031
|
};
|
|
1909
2032
|
|
|
1910
2033
|
/**
|
|
@@ -2101,7 +2224,7 @@ class TapsilatSDK {
|
|
|
2101
2224
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2102
2225
|
get webhooks() {
|
|
2103
2226
|
return {
|
|
2104
|
-
verify: (payload, signature, secret) => this.verifyWebhook(payload, signature, secret),
|
|
2227
|
+
verify: (payload, signature, secret, options) => this.verifyWebhook(payload, signature, secret, options),
|
|
2105
2228
|
};
|
|
2106
2229
|
}
|
|
2107
2230
|
// ORDER CREATION
|
|
@@ -2844,28 +2967,31 @@ class TapsilatSDK {
|
|
|
2844
2967
|
throw new TapsilatError("Checkout URL not found in order response", "CHECKOUT_URL_NOT_FOUND");
|
|
2845
2968
|
}
|
|
2846
2969
|
// WEBHOOK SIGNATURE VERIFICATION
|
|
2847
|
-
// Summary:
|
|
2848
|
-
// Description:
|
|
2970
|
+
// Summary: Verify the Tapsilat-Signature header of a callback
|
|
2971
|
+
// Description: Checks the timestamped HMAC-SHA256 that Tapsilat signs every callback with
|
|
2849
2972
|
/**
|
|
2850
|
-
* Verifies
|
|
2973
|
+
* Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
|
|
2851
2974
|
*
|
|
2852
|
-
* @summary
|
|
2853
|
-
* @description
|
|
2975
|
+
* @summary Verify the Tapsilat-Signature header of a callback
|
|
2976
|
+
* @description
|
|
2977
|
+
* Tapsilat sends "t=<unix timestamp>,v1=<hex HMAC-SHA256 of '<timestamp>.<raw body>'>". The timestamp
|
|
2978
|
+
* must be within `options.tolerance` seconds (300 by default; 0 disables the check) of `options.now`
|
|
2979
|
+
* (the current time by default) so that a captured callback cannot be replayed. The comparison takes
|
|
2980
|
+
* constant time.
|
|
2981
|
+
*
|
|
2982
|
+
* A bare hex HMAC-SHA256 of the payload, with or without a "sha256=" prefix, is still accepted for
|
|
2983
|
+
* signatures made the way earlier versions of this SDK documented.
|
|
2854
2984
|
*
|
|
2855
|
-
* @param payload -
|
|
2856
|
-
* @param signature -
|
|
2857
|
-
* @param secret -
|
|
2858
|
-
* @
|
|
2859
|
-
* @
|
|
2985
|
+
* @param payload - The raw request body as a string or Buffer, before any JSON decoding
|
|
2986
|
+
* @param signature - The Tapsilat-Signature header
|
|
2987
|
+
* @param secret - The webhook secret from the panel's callback settings
|
|
2988
|
+
* @param options - Timestamp tolerance in seconds and the current Unix time
|
|
2989
|
+
* @returns Promise resolving to true when the signature is valid, and to false when the header or
|
|
2990
|
+
* secret is empty, or the signature is malformed, stale or wrong
|
|
2991
|
+
* @throws {TapsilatValidationError} When payload is not a string or bytes, or an option is not a number
|
|
2860
2992
|
*/
|
|
2861
|
-
async verifyWebhook(payload, signature, secret) {
|
|
2862
|
-
|
|
2863
|
-
throw new TapsilatValidationError("Webhook payload is required and must be a non-empty string");
|
|
2864
|
-
if (!isNonEmptyString(signature))
|
|
2865
|
-
throw new TapsilatValidationError("Webhook signature is required and must be a non-empty string");
|
|
2866
|
-
if (!isNonEmptyString(secret))
|
|
2867
|
-
throw new TapsilatValidationError("Webhook secret is required and must be a non-empty string");
|
|
2868
|
-
return verifyHmacSignature(payload, signature, secret);
|
|
2993
|
+
async verifyWebhook(payload, signature, secret, options = {}) {
|
|
2994
|
+
return verifyWebhookSignature(payload, signature, secret, options);
|
|
2869
2995
|
}
|
|
2870
2996
|
// API HEALTH STATUS CHECK
|
|
2871
2997
|
// Summary: Check API service health and availability
|
|
@@ -3625,6 +3751,81 @@ class TapsilatSDK {
|
|
|
3625
3751
|
return handleError(error, "get installment table");
|
|
3626
3752
|
}
|
|
3627
3753
|
}
|
|
3754
|
+
// ELEMENTS SESSIONS
|
|
3755
|
+
// Summary: Create, read and cancel sessions for the Tapsilat Elements card sheet
|
|
3756
|
+
// Description: Your backend creates a session and hands only its client_secret to the app that opens the sheet
|
|
3757
|
+
/**
|
|
3758
|
+
* Creates a Tapsilat Elements session for the native card sheet
|
|
3759
|
+
*
|
|
3760
|
+
* @summary Create an Elements session
|
|
3761
|
+
* @description
|
|
3762
|
+
* Sends POST /elements/sessions. Hand the returned `client_secret` to your app, which opens the card
|
|
3763
|
+
* sheet with it, and do not log or store it. A `pay` session needs `order_reference_id`; a `tokenize`
|
|
3764
|
+
* session saves a card for `user_id` with the given `mode`.
|
|
3765
|
+
*
|
|
3766
|
+
* @param request - The session's purpose and its order or card owner
|
|
3767
|
+
* @returns The new session with its `client_secret` and `elements_base_url`
|
|
3768
|
+
* @throws {TapsilatValidationError} When `purpose` is missing
|
|
3769
|
+
* @throws {TapsilatError} When the API rejects the request; `apiCode` and `retryable` say why
|
|
3770
|
+
*/
|
|
3771
|
+
async createElementsSession(request) {
|
|
3772
|
+
if (!isNonEmptyString(request?.purpose)) {
|
|
3773
|
+
throw new TapsilatValidationError("Elements session purpose is required and must be a non-empty string", { provided: request?.purpose });
|
|
3774
|
+
}
|
|
3775
|
+
const body = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== undefined && value !== null));
|
|
3776
|
+
try {
|
|
3777
|
+
const response = await this.httpClient.post("/elements/sessions", body);
|
|
3778
|
+
return redactClientSecret(handleResponse(response, "Create Elements session"));
|
|
3779
|
+
}
|
|
3780
|
+
catch (error) {
|
|
3781
|
+
return handleError(error, "create Elements session");
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Returns the authoritative state and outcome of an Elements session
|
|
3786
|
+
*
|
|
3787
|
+
* @summary Get an Elements session
|
|
3788
|
+
* @description
|
|
3789
|
+
* Sends GET /elements/sessions/{id}. Read the outcome here on your backend before fulfilling an
|
|
3790
|
+
* order, rather than trusting what the app reports.
|
|
3791
|
+
*
|
|
3792
|
+
* @param id - Session ID, such as "es_test_8Kq2nV0rX4"
|
|
3793
|
+
* @throws {TapsilatError} With `apiCode` "session_not_found" and `statusCode` 404 for an unknown session
|
|
3794
|
+
*/
|
|
3795
|
+
async getElementsSession(id) {
|
|
3796
|
+
if (!isNonEmptyString(id)) {
|
|
3797
|
+
throw new TapsilatValidationError("Elements session ID is required and must be a non-empty string", { provided: id });
|
|
3798
|
+
}
|
|
3799
|
+
try {
|
|
3800
|
+
const response = await this.httpClient.get(`/elements/sessions/${encodeURIComponent(id)}`);
|
|
3801
|
+
return handleResponse(response, "Get Elements session");
|
|
3802
|
+
}
|
|
3803
|
+
catch (error) {
|
|
3804
|
+
return handleError(error, "get Elements session");
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
/**
|
|
3808
|
+
* Cancels an Elements session that is still waiting for a card
|
|
3809
|
+
*
|
|
3810
|
+
* @summary Cancel an Elements session
|
|
3811
|
+
* @description
|
|
3812
|
+
* Sends POST /elements/sessions/{id}/cancel with no body. A session that is confirming or has
|
|
3813
|
+
* completed comes back unchanged, so check `status` in the result.
|
|
3814
|
+
*
|
|
3815
|
+
* @param id - Session ID, such as "es_test_8Kq2nV0rX4"
|
|
3816
|
+
*/
|
|
3817
|
+
async cancelElementsSession(id) {
|
|
3818
|
+
if (!isNonEmptyString(id)) {
|
|
3819
|
+
throw new TapsilatValidationError("Elements session ID is required and must be a non-empty string", { provided: id });
|
|
3820
|
+
}
|
|
3821
|
+
try {
|
|
3822
|
+
const response = await this.httpClient.post(`/elements/sessions/${encodeURIComponent(id)}/cancel`);
|
|
3823
|
+
return handleResponse(response, "Cancel Elements session");
|
|
3824
|
+
}
|
|
3825
|
+
catch (error) {
|
|
3826
|
+
return handleError(error, "cancel Elements session");
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3628
3829
|
// ORGANIZATION CURRENCIES, USERS AND SUBORGANIZATIONS
|
|
3629
3830
|
async listOrganizationCurrencyPresets() {
|
|
3630
3831
|
try {
|
|
@@ -4073,5 +4274,319 @@ var BusinessType;
|
|
|
4073
4274
|
BusinessType[BusinessType["UNKNOWN"] = 4] = "UNKNOWN";
|
|
4074
4275
|
})(BusinessType || (BusinessType = {}));
|
|
4075
4276
|
|
|
4076
|
-
|
|
4277
|
+
/**
|
|
4278
|
+
* Request and response types for Tapsilat Elements sessions.
|
|
4279
|
+
*
|
|
4280
|
+
* Tapsilat Elements is the native Android and iOS card sheet. Your backend creates a session with
|
|
4281
|
+
* its secret API key and hands only the session's `client_secret` to the app, which opens the sheet
|
|
4282
|
+
* with it. Card data goes from the sheet to Tapsilat encrypted and never reaches your backend.
|
|
4283
|
+
*
|
|
4284
|
+
* Field names match the JSON wire format. Response fields are optional because the server omits
|
|
4285
|
+
* empty values. `purpose`, `status` and `brand` are plain strings so that values added later still
|
|
4286
|
+
* decode; compare them with the constants below.
|
|
4287
|
+
*/
|
|
4288
|
+
/**
|
|
4289
|
+
* @category Elements
|
|
4290
|
+
* @summary Purposes of an Elements session
|
|
4291
|
+
*/
|
|
4292
|
+
const ElementsSessionPurpose = Object.freeze({
|
|
4293
|
+
/** Save a card for a user without charging it */
|
|
4294
|
+
TOKENIZE: "tokenize",
|
|
4295
|
+
/** Pay an existing order */
|
|
4296
|
+
PAY: "pay",
|
|
4297
|
+
});
|
|
4298
|
+
/**
|
|
4299
|
+
* @category Elements
|
|
4300
|
+
* @summary Statuses of an Elements session
|
|
4301
|
+
*/
|
|
4302
|
+
const ElementsSessionStatus = Object.freeze({
|
|
4303
|
+
REQUIRES_PAYMENT_METHOD: "requires_payment_method",
|
|
4304
|
+
PROCESSING: "processing",
|
|
4305
|
+
REQUIRES_ACTION: "requires_action",
|
|
4306
|
+
SUCCEEDED: "succeeded",
|
|
4307
|
+
FAILED: "failed",
|
|
4308
|
+
EXPIRED: "expired",
|
|
4309
|
+
CANCELED: "canceled",
|
|
4310
|
+
});
|
|
4311
|
+
/**
|
|
4312
|
+
* @category Elements
|
|
4313
|
+
* @summary Tokenize modes of an Elements session
|
|
4314
|
+
*/
|
|
4315
|
+
const ElementsTokenizeMode = Object.freeze({
|
|
4316
|
+
/** 3D Secure */
|
|
4317
|
+
SECURE: 1,
|
|
4318
|
+
HALF_SECURE: 2,
|
|
4319
|
+
NON_SECURE: 3,
|
|
4320
|
+
});
|
|
4321
|
+
/**
|
|
4322
|
+
* @category Elements
|
|
4323
|
+
* @summary Card brands an Elements session reports
|
|
4324
|
+
*/
|
|
4325
|
+
const ElementsCardBrand = Object.freeze({
|
|
4326
|
+
VISA: "visa",
|
|
4327
|
+
MASTERCARD: "mastercard",
|
|
4328
|
+
AMEX: "amex",
|
|
4329
|
+
TROY: "troy",
|
|
4330
|
+
DISCOVER: "discover",
|
|
4331
|
+
UNIONPAY: "unionpay",
|
|
4332
|
+
UNKNOWN: "unknown",
|
|
4333
|
+
});
|
|
4334
|
+
|
|
4335
|
+
const CODES = {
|
|
4336
|
+
RECEIVED: 1,
|
|
4337
|
+
UNPAID: 2,
|
|
4338
|
+
PAID: 3,
|
|
4339
|
+
PROCESSING: 4,
|
|
4340
|
+
SHIPPED: 5,
|
|
4341
|
+
ON_HOLD: 6,
|
|
4342
|
+
WAITING_FOR_PAYMENT: 7,
|
|
4343
|
+
CANCELLED: 8,
|
|
4344
|
+
COMPLETED: 9,
|
|
4345
|
+
REFUNDED: 10,
|
|
4346
|
+
FRAUD: 11,
|
|
4347
|
+
REJECTED: 12,
|
|
4348
|
+
FAILURE: 13,
|
|
4349
|
+
RETRYING: 14,
|
|
4350
|
+
PARTIALLY_REFUNDED: 15,
|
|
4351
|
+
SUB_MERCHANT_PAYMENT_APPROVED: 16,
|
|
4352
|
+
SUB_MERCHANT_PAYMENT_DISAPPROVED: 17,
|
|
4353
|
+
SUB_MERCHANT_PAYMENT_ERRORED: 18,
|
|
4354
|
+
STILL_HAS_UNPAID_INSTALLMENTS: 19,
|
|
4355
|
+
STILL_HAS_UNPAID_TERMS: 20,
|
|
4356
|
+
EXPIRED: 21,
|
|
4357
|
+
STILL_HAS_UNPAID_SUB_MERCHANT_PAYMENTS: 22,
|
|
4358
|
+
PARTIALLY_PAID: 23,
|
|
4359
|
+
TERMINATED: 24,
|
|
4360
|
+
CARD_TOKENIZATION: 25,
|
|
4361
|
+
PRE_AUTHORIZED: 26,
|
|
4362
|
+
DISPUTED: 27,
|
|
4363
|
+
PARTIALLY_DISPUTED: 28,
|
|
4364
|
+
SUSPECT: 29,
|
|
4365
|
+
};
|
|
4366
|
+
/** The status names that GET /order/{reference_id}/status returns */
|
|
4367
|
+
const NAMES = Object.freeze({
|
|
4368
|
+
[CODES.RECEIVED]: "Received",
|
|
4369
|
+
[CODES.UNPAID]: "Unpaid",
|
|
4370
|
+
[CODES.PAID]: "Paid",
|
|
4371
|
+
[CODES.PROCESSING]: "Processing",
|
|
4372
|
+
[CODES.SHIPPED]: "Shipped",
|
|
4373
|
+
[CODES.ON_HOLD]: "On hold",
|
|
4374
|
+
[CODES.WAITING_FOR_PAYMENT]: "Waiting for payment",
|
|
4375
|
+
[CODES.CANCELLED]: "Cancelled",
|
|
4376
|
+
[CODES.COMPLETED]: "Completed",
|
|
4377
|
+
[CODES.REFUNDED]: "Refunded",
|
|
4378
|
+
[CODES.FRAUD]: "Fraud",
|
|
4379
|
+
[CODES.REJECTED]: "Rejected",
|
|
4380
|
+
[CODES.FAILURE]: "Failure",
|
|
4381
|
+
[CODES.RETRYING]: "Retrying",
|
|
4382
|
+
[CODES.PARTIALLY_REFUNDED]: "Partially refunded",
|
|
4383
|
+
[CODES.SUB_MERCHANT_PAYMENT_APPROVED]: "Sub merchant payment approved",
|
|
4384
|
+
[CODES.SUB_MERCHANT_PAYMENT_DISAPPROVED]: "Sub merchant payment disapproved",
|
|
4385
|
+
[CODES.SUB_MERCHANT_PAYMENT_ERRORED]: "Sub merchant payment errored",
|
|
4386
|
+
[CODES.STILL_HAS_UNPAID_INSTALLMENTS]: "Still has unpaid installments",
|
|
4387
|
+
[CODES.STILL_HAS_UNPAID_TERMS]: "Still has unpaid terms",
|
|
4388
|
+
[CODES.EXPIRED]: "Expired",
|
|
4389
|
+
[CODES.STILL_HAS_UNPAID_SUB_MERCHANT_PAYMENTS]: "Still has unpaid sub merchant payments",
|
|
4390
|
+
[CODES.PARTIALLY_PAID]: "Partially Paid",
|
|
4391
|
+
[CODES.TERMINATED]: "Terminated",
|
|
4392
|
+
[CODES.CARD_TOKENIZATION]: "Card Tokenization",
|
|
4393
|
+
[CODES.PRE_AUTHORIZED]: "Pre Authorized",
|
|
4394
|
+
[CODES.DISPUTED]: "Disputed",
|
|
4395
|
+
[CODES.PARTIALLY_DISPUTED]: "Partially disputed",
|
|
4396
|
+
[CODES.SUSPECT]: "Suspect",
|
|
4397
|
+
});
|
|
4398
|
+
/** Statuses of an order whose payment was captured */
|
|
4399
|
+
const PAID_STATUSES = Object.freeze([
|
|
4400
|
+
CODES.PAID,
|
|
4401
|
+
CODES.COMPLETED,
|
|
4402
|
+
CODES.SUB_MERCHANT_PAYMENT_APPROVED,
|
|
4403
|
+
]);
|
|
4404
|
+
/** Statuses of an order that will not be paid */
|
|
4405
|
+
const FAILED_STATUSES = Object.freeze([
|
|
4406
|
+
CODES.CANCELLED,
|
|
4407
|
+
CODES.FRAUD,
|
|
4408
|
+
CODES.REJECTED,
|
|
4409
|
+
CODES.FAILURE,
|
|
4410
|
+
CODES.EXPIRED,
|
|
4411
|
+
CODES.TERMINATED,
|
|
4412
|
+
]);
|
|
4413
|
+
/** Statuses of an order whose payment was refunded in full or in part */
|
|
4414
|
+
const REFUNDED_STATUSES = Object.freeze([CODES.REFUNDED, CODES.PARTIALLY_REFUNDED]);
|
|
4415
|
+
const isCode = (value) => Object.prototype.hasOwnProperty.call(NAMES, value);
|
|
4416
|
+
/**
|
|
4417
|
+
* Returns the status code for a code, a numeric string such as order_lite callbacks send, or a status
|
|
4418
|
+
* name such as "Paid" (case-insensitive), or null when the value is not a status
|
|
4419
|
+
*/
|
|
4420
|
+
const code = (status) => {
|
|
4421
|
+
if (typeof status === "number") {
|
|
4422
|
+
return Number.isInteger(status) && isCode(status) ? status : null;
|
|
4423
|
+
}
|
|
4424
|
+
if (typeof status !== "string") {
|
|
4425
|
+
return null;
|
|
4426
|
+
}
|
|
4427
|
+
const trimmed = status.trim();
|
|
4428
|
+
if (/^[0-9]+$/.test(trimmed)) {
|
|
4429
|
+
return code(Number(trimmed));
|
|
4430
|
+
}
|
|
4431
|
+
const lower = trimmed.toLowerCase();
|
|
4432
|
+
for (const [value, label] of Object.entries(NAMES)) {
|
|
4433
|
+
if (label.toLowerCase() === lower) {
|
|
4434
|
+
return Number(value);
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
return null;
|
|
4438
|
+
};
|
|
4439
|
+
/** Returns the status name, such as "Paid", or null when the value is not a status */
|
|
4440
|
+
const name = (status) => {
|
|
4441
|
+
const statusCode = code(status);
|
|
4442
|
+
return statusCode === null ? null : NAMES[statusCode];
|
|
4443
|
+
};
|
|
4444
|
+
const inGroup = (group, status) => {
|
|
4445
|
+
const statusCode = code(status);
|
|
4446
|
+
return statusCode !== null && group.includes(statusCode);
|
|
4447
|
+
};
|
|
4448
|
+
/**
|
|
4449
|
+
* @category Order Management
|
|
4450
|
+
* @summary Order status codes, their names and the paid, failed and refunded groups
|
|
4451
|
+
* @description
|
|
4452
|
+
* The numeric codes that callbacks and order details carry, and the names that
|
|
4453
|
+
* GET /order/{reference_id}/status returns for them. Every helper accepts a code, a numeric string or
|
|
4454
|
+
* a name.
|
|
4455
|
+
*
|
|
4456
|
+
* @example
|
|
4457
|
+
* ```typescript
|
|
4458
|
+
* const { status } = await tapsilat.getOrderStatus(referenceId); // e.g. "Waiting for payment"
|
|
4459
|
+
* OrderStatus.isPaid(status); // Paid, Completed, Sub merchant payment approved
|
|
4460
|
+
* OrderStatus.code(status); // 7; also accepts "7" or 7
|
|
4461
|
+
* ```
|
|
4462
|
+
*/
|
|
4463
|
+
const OrderStatus = Object.freeze({
|
|
4464
|
+
...CODES,
|
|
4465
|
+
NAMES,
|
|
4466
|
+
PAID_STATUSES,
|
|
4467
|
+
FAILED_STATUSES,
|
|
4468
|
+
REFUNDED_STATUSES,
|
|
4469
|
+
/** The code for a code, a numeric string or a name (case-insensitive), or null */
|
|
4470
|
+
code,
|
|
4471
|
+
/** The status name, such as "Paid", or null */
|
|
4472
|
+
name,
|
|
4473
|
+
/** Paid, Completed or Sub merchant payment approved */
|
|
4474
|
+
isPaid: (status) => inGroup(PAID_STATUSES, status),
|
|
4475
|
+
/** Cancelled, Fraud, Rejected, Failure, Expired or Terminated */
|
|
4476
|
+
isFailed: (status) => inGroup(FAILED_STATUSES, status),
|
|
4477
|
+
/** Refunded or Partially refunded */
|
|
4478
|
+
isRefunded: (status) => inGroup(REFUNDED_STATUSES, status),
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4481
|
+
const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
|
4482
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4483
|
+
const firstString = (values) => {
|
|
4484
|
+
for (const value of values) {
|
|
4485
|
+
if (typeof value === "string" && value !== "" && value !== ZERO_UUID) {
|
|
4486
|
+
return value;
|
|
4487
|
+
}
|
|
4488
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
4489
|
+
return String(value);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
return null;
|
|
4493
|
+
};
|
|
4494
|
+
/**
|
|
4495
|
+
* @category Webhooks
|
|
4496
|
+
* @summary A callback Tapsilat posts to an organization's callback URLs
|
|
4497
|
+
* @description
|
|
4498
|
+
* Tapsilat posts {"type": "...", "data": {...}, "token": "<delivery id>"} to the organization's callback
|
|
4499
|
+
* URLs and to an order's payment_success_url and payment_failure_url. The shape of "data" depends on the
|
|
4500
|
+
* type, which each organization chooses in the panel: "order_lite" (the default), "order",
|
|
4501
|
+
* "order_payment_detail", "extended_order" or "payment_facilitator". The getters read the order's
|
|
4502
|
+
* reference ID, conversation ID and status from whichever shape arrived.
|
|
4503
|
+
*
|
|
4504
|
+
* Verify the Tapsilat-Signature header with `verifyWebhook()` before trusting a callback.
|
|
4505
|
+
*
|
|
4506
|
+
* @example
|
|
4507
|
+
* ```typescript
|
|
4508
|
+
* const event = WebhookEvent.fromPayload(rawBody);
|
|
4509
|
+
* if (event.isPaid()) {
|
|
4510
|
+
* await fulfil(event.referenceId);
|
|
4511
|
+
* }
|
|
4512
|
+
* ```
|
|
4513
|
+
*/
|
|
4514
|
+
class WebhookEvent {
|
|
4515
|
+
static TYPE_ORDER = "order";
|
|
4516
|
+
static TYPE_ORDER_LITE = "order_lite";
|
|
4517
|
+
static TYPE_ORDER_PAYMENT_DETAIL = "order_payment_detail";
|
|
4518
|
+
static TYPE_EXTENDED_ORDER = "extended_order";
|
|
4519
|
+
static TYPE_PAYMENT_FACILITATOR = "payment_facilitator";
|
|
4520
|
+
/** The callback type, such as "order_lite" */
|
|
4521
|
+
type;
|
|
4522
|
+
/** Unique per delivery */
|
|
4523
|
+
token;
|
|
4524
|
+
/** The callback's "data" object, or the whole payload when it has none */
|
|
4525
|
+
data;
|
|
4526
|
+
/** The decoded payload */
|
|
4527
|
+
payload;
|
|
4528
|
+
constructor(payload) {
|
|
4529
|
+
this.payload = payload;
|
|
4530
|
+
this.type = typeof payload.type === "string" ? payload.type : null;
|
|
4531
|
+
this.token = typeof payload.token === "string" ? payload.token : null;
|
|
4532
|
+
this.data = isObject(payload.data) ? payload.data : payload;
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* Decodes a raw callback body
|
|
4536
|
+
*
|
|
4537
|
+
* @param payload - The raw request body
|
|
4538
|
+
* @throws {TapsilatValidationError} When the body is not a JSON object
|
|
4539
|
+
*/
|
|
4540
|
+
static fromPayload(payload) {
|
|
4541
|
+
const body = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
4542
|
+
let decoded;
|
|
4543
|
+
try {
|
|
4544
|
+
decoded = JSON.parse(body);
|
|
4545
|
+
}
|
|
4546
|
+
catch {
|
|
4547
|
+
decoded = undefined;
|
|
4548
|
+
}
|
|
4549
|
+
if (!isObject(decoded)) {
|
|
4550
|
+
throw new TapsilatValidationError("The webhook payload is not a JSON object");
|
|
4551
|
+
}
|
|
4552
|
+
return new WebhookEvent(decoded);
|
|
4553
|
+
}
|
|
4554
|
+
/** The order's reference ID */
|
|
4555
|
+
get referenceId() {
|
|
4556
|
+
const order = isObject(this.data.order) ? this.data.order : {};
|
|
4557
|
+
return firstString([
|
|
4558
|
+
this.data.orderReferenceId,
|
|
4559
|
+
this.data.order_reference_id,
|
|
4560
|
+
this.data.reference_id,
|
|
4561
|
+
order.reference_id,
|
|
4562
|
+
]);
|
|
4563
|
+
}
|
|
4564
|
+
/** The conversation ID the order was created with */
|
|
4565
|
+
get conversationId() {
|
|
4566
|
+
return firstString([this.data.conversationId, this.data.conversation_id]);
|
|
4567
|
+
}
|
|
4568
|
+
/** The order status code (see OrderStatus), or null when this callback type carries none */
|
|
4569
|
+
get statusCode() {
|
|
4570
|
+
const code = OrderStatus.code(this.data.status);
|
|
4571
|
+
if (code === null && isObject(this.data.order)) {
|
|
4572
|
+
return OrderStatus.code(this.data.order.status);
|
|
4573
|
+
}
|
|
4574
|
+
return code;
|
|
4575
|
+
}
|
|
4576
|
+
/** The order status name, such as "Paid" */
|
|
4577
|
+
get status() {
|
|
4578
|
+
return OrderStatus.name(this.statusCode);
|
|
4579
|
+
}
|
|
4580
|
+
isPaid() {
|
|
4581
|
+
return OrderStatus.isPaid(this.statusCode);
|
|
4582
|
+
}
|
|
4583
|
+
isFailed() {
|
|
4584
|
+
return OrderStatus.isFailed(this.statusCode);
|
|
4585
|
+
}
|
|
4586
|
+
isRefunded() {
|
|
4587
|
+
return OrderStatus.isRefunded(this.statusCode);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
export { BusinessType, ConfigManager, CurrencyResolver, ElementsCardBrand, ElementsSessionPurpose, ElementsSessionStatus, ElementsTokenizeMode, MarketplaceService, OrderStatus, SubmerchantService, TapsilatAuthenticationError, TapsilatError, TapsilatNetworkError, TapsilatRateLimitError, TapsilatSDK, TapsilatValidationError, TokenizationService, VposService, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TOLERANCE, WebhookEvent, cleanGsmNumber, isValidCurrency, isValidPaymentMethod, isValidUrl, sanitizeMetadata, validateEmail, validateGsmNumber, validateInstallments, validatePaymentRequest };
|
|
4077
4592
|
//# sourceMappingURL=index.js.map
|