@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/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: body.code !== undefined && body.code !== null
533
- ? String(body.code)
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
- return (obj.message || obj.error || obj.detail);
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: Security verification of webhook payloads using HMAC-SHA256
1888
- // Description: Computes expected signature from payload and secret, compares with provided signature
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 HMAC-SHA256 signature for webhook security
1968
+ * Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
1891
1969
  *
1892
- * @summary Security verification of webhook payloads using HMAC-SHA256
1893
- * @description Computes expected signature from payload and secret, compares with provided signature
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 - Raw webhook payload string
1896
- * @param signature - Webhook signature from headers (should include "sha256=" prefix)
1897
- * @param secret - Your webhook secret key
1898
- * @returns true if signature is valid, false otherwise
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 verifyHmacSignature = (payload, signature, secret) => {
1901
- const expectedSignature = crypto
1902
- .createHmac("sha256", secret)
1903
- .update(payload)
1904
- .digest("hex");
1905
- const expected = Buffer.from(`sha256=${expectedSignature}`);
1906
- const provided = Buffer.from(signature ?? "");
1907
- // Compare in constant time so the signature cannot be guessed byte by byte
1908
- return (expected.length === provided.length &&
1909
- crypto.timingSafeEqual(expected, provided));
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: Validate webhook signature for security verification
2850
- // Description: Verifies the authenticity of webhook payloads using HMAC signature validation
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 webhook signature for security
2975
+ * Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
2853
2976
  *
2854
- * @summary Validate webhook signature for security verification
2855
- * @description Verifies the authenticity of webhook payloads using HMAC signature validation.
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 - Raw webhook payload string
2858
- * @param signature - Webhook signature from headers
2859
- * @param secret - Your webhook secret key
2860
- * @returns Promise resolving to true if signature is valid
2861
- * @throws {TapsilatValidationError} When input validation fails for payload, signature or secret
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
- if (!isNonEmptyString(payload))
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;