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