@basis-theory/web-elements 3.0.0 → 3.1.0

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.
@@ -150,6 +150,8 @@ const generateId = () => {
150
150
  const randomPart = (random() + random() + random() + random()).substring(0, 32);
151
151
  return `${randomPart.substring(0, 8)}-${randomPart.substring(8, 12)}-${randomPart.substring(12, 16)}-${randomPart.substring(16, 20)}-${randomPart.substring(20, 32)}`;
152
152
  };
153
+ const LANGUAGE_TAG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*$/;
154
+ const isLanguageTag = (value) => typeof value === "string" && LANGUAGE_TAG.test(value);
153
155
  const log = (action, metadata) => {
154
156
  };
155
157
  const createBasisTheoryError = (code, message, details) => {
@@ -195,6 +197,16 @@ const ConfigurationError = (message, details) => {
195
197
  error.name = "ConfigurationError";
196
198
  return error;
197
199
  };
200
+ const HttpClientError = (message, status, data, headers) => {
201
+ const error = createBasisTheoryError("HTTP_CLIENT_ERROR", message, {
202
+ status
203
+ });
204
+ error.name = "HttpClientError";
205
+ error.status = status;
206
+ error.data = data;
207
+ error.headers = headers;
208
+ return error;
209
+ };
198
210
  const ALLOWED_PROXY_SCHEMES = /* @__PURE__ */ new Set(["https:"]);
199
211
  const ALLOWED_METHODS = /* @__PURE__ */ new Set([
200
212
  "GET",
@@ -381,6 +393,35 @@ const validateProxyTarget = (url, proxyKey) => {
381
393
  validateProxyUrl(url);
382
394
  }
383
395
  };
396
+ const FORBIDDEN_DESTINATION_HEADERS = new Set([...FORBIDDEN_PROXY_HEADERS].filter((header) => header !== "content-type"));
397
+ const validateDestinationHeaders = (headers) => {
398
+ if (!headers)
399
+ return;
400
+ for (const [key, value] of Object.entries(headers)) {
401
+ if (/[\r\n]/.test(value)) {
402
+ throw ValidationError(`Header '${key}' value contains illegal CRLF`, "headers");
403
+ }
404
+ if (/[\r\n]/.test(key)) {
405
+ throw ValidationError(`Header name '${key}' contains illegal CRLF`, "headers");
406
+ }
407
+ if (FORBIDDEN_DESTINATION_HEADERS.has(key.toLowerCase().trim())) {
408
+ throw ValidationError(`Header '${key}' is not allowed in HTTP client requests`, "headers");
409
+ }
410
+ }
411
+ };
412
+ const validateDestinationRequest = (options) => {
413
+ if (options !== void 0 && (typeof options !== "object" || options === null)) {
414
+ throw ValidationError("HTTP client options must be a plain object", "options");
415
+ }
416
+ if ((options == null ? void 0 : options.method) !== void 0 && !ALLOWED_METHODS.has(options.method)) {
417
+ throw ValidationError(`Method '${options.method}' is not allowed`, "method");
418
+ }
419
+ if (typeof (options == null ? void 0 : options.url) !== "string" || options.url.length === 0) {
420
+ throw ValidationError("url is required", "url");
421
+ }
422
+ validateProxyUrl(options.url);
423
+ validateDestinationHeaders(options == null ? void 0 : options.headers);
424
+ };
384
425
  const validateProxyOptions = (options) => {
385
426
  if (options !== void 0 && (typeof options !== "object" || options === null)) {
386
427
  throw ValidationError("Proxy options must be a plain object", "options");
@@ -450,6 +491,8 @@ const createPostMessageClient = (iframe, config) => {
450
491
  ...error.retryCount !== void 0 && {
451
492
  retryCount: error.retryCount
452
493
  },
494
+ ...error.data !== void 0 && { data: error.data },
495
+ ...error.headers && { headers: error.headers },
453
496
  ...error.details && { details: error.details }
454
497
  });
455
498
  pending.reject(reconstructedError);
@@ -878,17 +921,21 @@ const createCardNumberElement = (apiKey, sdkConfig, options = {}, coordinatorEle
878
921
  elementId: id,
879
922
  // Include element ID for validation
880
923
  apiKey,
924
+ environment: sdkConfig.environment,
881
925
  apiTimeout: sdkConfig.timeoutMs,
882
926
  apiRetries: sdkConfig.retryConfig.maxRetries,
883
927
  theme: sdkConfig.theme,
884
928
  darkTheme: sdkConfig.darkTheme,
885
929
  themeMode: sdkConfig.themeMode,
930
+ direction: sdkConfig.direction,
931
+ language: sdkConfig.language,
886
932
  placeholder: options.placeholder,
887
933
  disabled: options.disabled,
888
934
  readOnly: options.readOnly,
889
935
  debug: sdkConfig.debug,
890
936
  binLookup: options.binLookup,
891
- coBadge: options.coBadge
937
+ coBadge: options.coBadge,
938
+ enterKeyHint: options.enterKeyHint
892
939
  });
893
940
  if (iframe) iframe.style.opacity = "1";
894
941
  } catch (error) {
@@ -959,6 +1006,9 @@ const createCardNumberElement = (apiKey, sdkConfig, options = {}, coordinatorEle
959
1006
  if (newOptions.coBadge !== void 0) {
960
1007
  updatePayload.coBadge = newOptions.coBadge;
961
1008
  }
1009
+ if ("enterKeyHint" in newOptions) {
1010
+ updatePayload.enterKeyHint = newOptions.enterKeyHint;
1011
+ }
962
1012
  if (newOptions.themeMode !== void 0) {
963
1013
  updatePayload.themeMode = newOptions.themeMode;
964
1014
  updatePayload.theme = sdkConfig.theme;
@@ -1097,13 +1147,17 @@ const createCVVElement = (apiKey, sdkConfig, options = {}, coordinatorElementId,
1097
1147
  theme: sdkConfig.theme,
1098
1148
  darkTheme: sdkConfig.darkTheme,
1099
1149
  themeMode: sdkConfig.themeMode,
1150
+ direction: sdkConfig.direction,
1151
+ language: sdkConfig.language,
1100
1152
  apiKey,
1153
+ environment: sdkConfig.environment,
1101
1154
  apiTimeout: sdkConfig.timeoutMs,
1102
1155
  apiRetries: sdkConfig.retryConfig.maxRetries,
1103
1156
  placeholder: options.placeholder,
1104
1157
  disabled: options.disabled,
1105
1158
  readOnly: options.readOnly,
1106
1159
  showToggle: options.showToggle,
1160
+ enterKeyHint: options.enterKeyHint,
1107
1161
  debug: sdkConfig.debug
1108
1162
  });
1109
1163
  if (iframe) iframe.style.opacity = "1";
@@ -1160,6 +1214,11 @@ const createCVVElement = (apiKey, sdkConfig, options = {}, coordinatorElementId,
1160
1214
  placeholder: newOptions.placeholder,
1161
1215
  disabled: newOptions.disabled,
1162
1216
  readOnly: newOptions.readOnly,
1217
+ // Forwarded only when the caller named it, so an explicit
1218
+ // `enterKeyHint: undefined` clears the hint but a partial update keeps it
1219
+ ..."enterKeyHint" in newOptions && {
1220
+ enterKeyHint: newOptions.enterKeyHint
1221
+ },
1163
1222
  // ✅ SDK auto-coordination: Pass brand info for dynamic maxlength
1164
1223
  cardBrand: newOptions.cardBrand,
1165
1224
  cvvLengths: newOptions.cvvLengths
@@ -1296,12 +1355,16 @@ const createExpiryElement = (apiKey, sdkConfig, options = {}, coordinatorElement
1296
1355
  theme: sdkConfig.theme,
1297
1356
  darkTheme: sdkConfig.darkTheme,
1298
1357
  themeMode: sdkConfig.themeMode,
1358
+ direction: sdkConfig.direction,
1359
+ language: sdkConfig.language,
1299
1360
  apiKey,
1361
+ environment: sdkConfig.environment,
1300
1362
  apiTimeout: sdkConfig.timeoutMs,
1301
1363
  apiRetries: sdkConfig.retryConfig.maxRetries,
1302
1364
  placeholder: options.placeholder,
1303
1365
  disabled: options.disabled,
1304
1366
  readOnly: options.readOnly,
1367
+ enterKeyHint: options.enterKeyHint,
1305
1368
  debug: sdkConfig.debug
1306
1369
  });
1307
1370
  if (iframe) iframe.style.opacity = "1";
@@ -1357,7 +1420,12 @@ const createExpiryElement = (apiKey, sdkConfig, options = {}, coordinatorElement
1357
1420
  const updatePayload = {
1358
1421
  placeholder: newOptions.placeholder,
1359
1422
  disabled: newOptions.disabled,
1360
- readOnly: newOptions.readOnly
1423
+ readOnly: newOptions.readOnly,
1424
+ // Forwarded only when the caller named it, so an explicit
1425
+ // `enterKeyHint: undefined` clears the hint but a partial update keeps it
1426
+ ..."enterKeyHint" in newOptions && {
1427
+ enterKeyHint: newOptions.enterKeyHint
1428
+ }
1361
1429
  };
1362
1430
  if (newOptions.themeMode !== void 0) {
1363
1431
  updatePayload.themeMode = newOptions.themeMode;
@@ -1511,7 +1579,10 @@ const createTextElement = (apiKey, sdkConfig, options = {}, coordinatorElementId
1511
1579
  theme: sdkConfig.theme,
1512
1580
  darkTheme: sdkConfig.darkTheme,
1513
1581
  themeMode: sdkConfig.themeMode,
1582
+ direction: sdkConfig.direction,
1583
+ language: sdkConfig.language,
1514
1584
  apiKey,
1585
+ environment: sdkConfig.environment,
1515
1586
  apiTimeout: sdkConfig.timeoutMs,
1516
1587
  apiRetries: sdkConfig.retryConfig.maxRetries,
1517
1588
  placeholder: options.placeholder,
@@ -1523,6 +1594,7 @@ const createTextElement = (apiKey, sdkConfig, options = {}, coordinatorElementId
1523
1594
  maxLength: options.maxLength,
1524
1595
  password: options.password,
1525
1596
  inputMode: options.inputMode,
1597
+ enterKeyHint: options.enterKeyHint,
1526
1598
  mask: serializedMask,
1527
1599
  transform: serializedTransform,
1528
1600
  debug: sdkConfig.debug
@@ -1585,7 +1657,12 @@ const createTextElement = (apiKey, sdkConfig, options = {}, coordinatorElementId
1585
1657
  required: newOptions.required,
1586
1658
  maxLength: newOptions.maxLength,
1587
1659
  password: newOptions.password,
1588
- inputMode: newOptions.inputMode
1660
+ inputMode: newOptions.inputMode,
1661
+ // Forwarded only when the caller named it, so an explicit
1662
+ // `enterKeyHint: undefined` clears the hint but a partial update keeps it
1663
+ ..."enterKeyHint" in newOptions && {
1664
+ enterKeyHint: newOptions.enterKeyHint
1665
+ }
1589
1666
  };
1590
1667
  if (newOptions.themeMode !== void 0) {
1591
1668
  updatePayload.themeMode = newOptions.themeMode;
@@ -1811,16 +1888,21 @@ const warnUnknownKeys = (label, obj, knownKeys, deprecatedKeys) => {
1811
1888
  );
1812
1889
  }
1813
1890
  };
1891
+ const VALID_ENVIRONMENTS = ["us", "eu", "test"];
1814
1892
  const KNOWN_SDK_KEYS = /* @__PURE__ */ new Set([
1815
1893
  "elementsBaseUrl",
1816
1894
  "whitelabelDomain",
1817
1895
  "apiBaseUrl",
1896
+ "environment",
1818
1897
  "debug",
1819
1898
  "allowClipboard",
1899
+ "allowHttpClient",
1820
1900
  "measurePerformance",
1821
1901
  "timeoutMs",
1822
1902
  "retryConfig",
1823
1903
  "themeMode",
1904
+ "direction",
1905
+ "language",
1824
1906
  "theme",
1825
1907
  "darkTheme",
1826
1908
  "sessionAuthorizationUrl"
@@ -1868,6 +1950,9 @@ const validateSDKConfig = (options = {}) => {
1868
1950
  if (options.allowClipboard !== void 0 && typeof options.allowClipboard !== "boolean") {
1869
1951
  throw ConfigurationError("allowClipboard must be a boolean");
1870
1952
  }
1953
+ if (options.allowHttpClient !== void 0 && typeof options.allowHttpClient !== "boolean") {
1954
+ throw ConfigurationError("allowHttpClient must be a boolean");
1955
+ }
1871
1956
  if (options.measurePerformance !== void 0 && typeof options.measurePerformance !== "boolean") {
1872
1957
  throw ConfigurationError("measurePerformance must be a boolean");
1873
1958
  }
@@ -1894,6 +1979,20 @@ const validateSDKConfig = (options = {}) => {
1894
1979
  if (!["light", "dark", "auto"].includes(themeMode)) {
1895
1980
  throw ConfigurationError('themeMode must be "light", "dark", or "auto"');
1896
1981
  }
1982
+ if (options.environment !== void 0) {
1983
+ if (!VALID_ENVIRONMENTS.includes(options.environment)) {
1984
+ throw ConfigurationError(
1985
+ `environment must be ${VALID_ENVIRONMENTS.map((e) => `"${e}"`).join(", ")}`
1986
+ );
1987
+ }
1988
+ }
1989
+ const direction = options.direction ?? "ltr";
1990
+ if (!["ltr", "rtl"].includes(direction)) {
1991
+ throw ConfigurationError('direction must be "ltr" or "rtl"');
1992
+ }
1993
+ if (options.language !== void 0 && !isLanguageTag(options.language)) {
1994
+ throw ConfigurationError("language must be a BCP 47 language tag");
1995
+ }
1897
1996
  if (options.sessionAuthorizationUrl !== void 0) {
1898
1997
  if (typeof options.sessionAuthorizationUrl !== "string" || options.sessionAuthorizationUrl.trim() === "") {
1899
1998
  throw ConfigurationError("sessionAuthorizationUrl must be a non-empty string");
@@ -1928,22 +2027,27 @@ const validateSDKConfig = (options = {}) => {
1928
2027
  }
1929
2028
  throw ConfigurationError("whitelabelDomain must be a valid URL");
1930
2029
  }
1931
- const { pathname } = new URL("https://js.basistheory.com/3.0.0/elements");
2030
+ const { pathname } = new URL("https://js.basistheory.com/3.1.0/elements");
1932
2031
  resolvedBaseUrl = `${domainOrigin}${pathname}`;
1933
2032
  }
1934
- const finalBaseUrl = resolvedBaseUrl || "https://js.basistheory.com/3.0.0/elements";
2033
+ const finalBaseUrl = resolvedBaseUrl || "https://js.basistheory.com/3.1.0/elements";
1935
2034
  const iframeOrigin = new URL(finalBaseUrl).origin;
1936
2035
  return {
1937
2036
  iframeOrigin,
1938
2037
  elementsBaseUrl: finalBaseUrl,
1939
2038
  apiBaseUrl: options.apiBaseUrl,
1940
2039
  // Optional - auto-detects if not provided
2040
+ environment: options.environment,
2041
+ // Optional - resolves as before when unset
1941
2042
  debug: options.debug ?? false,
1942
2043
  allowClipboard: options.allowClipboard ?? false,
2044
+ allowHttpClient: options.allowHttpClient ?? false,
1943
2045
  measurePerformance: options.measurePerformance ?? false,
1944
2046
  timeoutMs: options.timeoutMs || 3e4,
1945
2047
  // 30s default for slow 3G connections
1946
2048
  themeMode,
2049
+ direction,
2050
+ language: options.language,
1947
2051
  theme: mergeWithDefaultTheme(options.theme),
1948
2052
  // Light theme or only theme
1949
2053
  darkTheme: options.darkTheme ? mergeWithDefaultTheme(options.darkTheme) : void 0,
@@ -1957,8 +2061,31 @@ const COMMON_ELEMENT_KEYS = /* @__PURE__ */ new Set([
1957
2061
  "placeholder",
1958
2062
  "ariaLabel",
1959
2063
  "disabled",
1960
- "readOnly"
2064
+ "readOnly",
2065
+ "enterKeyHint"
2066
+ ]);
2067
+ const VALID_ENTER_KEY_HINTS = /* @__PURE__ */ new Set([
2068
+ "enter",
2069
+ "done",
2070
+ "go",
2071
+ "next",
2072
+ "previous",
2073
+ "search",
2074
+ "send"
1961
2075
  ]);
2076
+ const CARD_ENTER_KEY_HINT_FIELDS = /* @__PURE__ */ new Set([
2077
+ "cardNumber",
2078
+ "expiryDate",
2079
+ "cvv",
2080
+ "cvc"
2081
+ ]);
2082
+ const validateEnterKeyHintValue = (value, label) => {
2083
+ if (typeof value !== "string" || !VALID_ENTER_KEY_HINTS.has(value)) {
2084
+ throw ConfigurationError(
2085
+ `${label} must be one of: ${Array.from(VALID_ENTER_KEY_HINTS).join(", ")}`
2086
+ );
2087
+ }
2088
+ };
1962
2089
  const ELEMENT_TYPE_KEYS = {
1963
2090
  cardNumber: /* @__PURE__ */ new Set([...COMMON_ELEMENT_KEYS, "binLookup", "coBadge"]),
1964
2091
  cvv: /* @__PURE__ */ new Set([...COMMON_ELEMENT_KEYS, "cardBrand", "cvvLengths"]),
@@ -2021,6 +2148,23 @@ const validateElementOptions = (type, options = {}) => {
2021
2148
  if (options.ariaLabel !== void 0 && typeof options.ariaLabel !== "string") {
2022
2149
  throw ConfigurationError("ariaLabel must be a string");
2023
2150
  }
2151
+ if (options.enterKeyHint !== void 0) {
2152
+ const enterKeyHint = options.enterKeyHint;
2153
+ if (type === "card" && typeof enterKeyHint === "object" && enterKeyHint !== null && !Array.isArray(enterKeyHint)) {
2154
+ warnUnknownKeys(
2155
+ "enterKeyHint",
2156
+ enterKeyHint,
2157
+ CARD_ENTER_KEY_HINT_FIELDS
2158
+ );
2159
+ Object.entries(enterKeyHint).filter(([field]) => CARD_ENTER_KEY_HINT_FIELDS.has(field)).forEach(([field, value]) => {
2160
+ if (value !== void 0) {
2161
+ validateEnterKeyHintValue(value, `enterKeyHint.${field}`);
2162
+ }
2163
+ });
2164
+ } else {
2165
+ validateEnterKeyHintValue(enterKeyHint, "enterKeyHint");
2166
+ }
2167
+ }
2024
2168
  if (type === "text") {
2025
2169
  if (options.maxLength !== void 0) {
2026
2170
  if (typeof options.maxLength !== "number" || options.maxLength <= 0) {
@@ -2227,21 +2371,21 @@ const BasisTheory = (apiKey, options = {}, queuedCommands) => {
2227
2371
  let cardDisplayElementFactory = null;
2228
2372
  const lazyCardElement = async () => {
2229
2373
  if (!cardElementFactory) {
2230
- const module = await import("./create-card-C94szxMV.js");
2374
+ const module = await import("./create-card-CoTyU6ju.js");
2231
2375
  cardElementFactory = module.createCardElement;
2232
2376
  }
2233
2377
  return cardElementFactory;
2234
2378
  };
2235
2379
  const lazyCopyButtonElement = async () => {
2236
2380
  if (!copyButtonElementFactory) {
2237
- const module = await import("./create-copy-button-Z1s9ao67.js");
2381
+ const module = await import("./create-copy-button-D8VeinHW.js");
2238
2382
  copyButtonElementFactory = module.createCopyButtonElement;
2239
2383
  }
2240
2384
  return copyButtonElementFactory;
2241
2385
  };
2242
2386
  const lazyCardDisplayElement = async () => {
2243
2387
  if (!cardDisplayElementFactory) {
2244
- const module = await import("./create-card-display-DsLzA_vl.js");
2388
+ const module = await import("./create-card-display-DQmTA_8S.js");
2245
2389
  cardDisplayElementFactory = module.createCardDisplayElement;
2246
2390
  }
2247
2391
  return cardDisplayElementFactory;
@@ -2336,11 +2480,15 @@ const BasisTheory = (apiKey, options = {}, queuedCommands) => {
2336
2480
  apiKey,
2337
2481
  apiBaseUrl: config.apiBaseUrl,
2338
2482
  // Optional - auto-detects if not provided
2483
+ environment: config.environment,
2484
+ // Optional - resolves as before when unset
2339
2485
  apiTimeout: 3e4,
2340
2486
  apiRetries: 3,
2341
2487
  // Frozen by the coordinator on this first config; copy references are
2342
2488
  // refused unless it's enabled here.
2343
2489
  allowClipboard: config.allowClipboard,
2490
+ // Frozen alongside allowClipboard; bt.client.* is refused unless enabled here.
2491
+ allowHttpClient: config.allowHttpClient,
2344
2492
  debug: config.debug || false,
2345
2493
  theme: config.theme,
2346
2494
  darkTheme: config.darkTheme,
@@ -2794,6 +2942,61 @@ const BasisTheory = (apiKey, options = {}, queuedCommands) => {
2794
2942
  delete: proxyVerb("DELETE")
2795
2943
  };
2796
2944
  })(),
2945
+ client: (() => {
2946
+ const invokeClient = guardAsync(async (method, url, body, options2 = {}) => {
2947
+ if (!config.allowHttpClient) {
2948
+ throw ConfigurationError(
2949
+ "bt.client requires allowHttpClient: true on BasisTheory() initialization. Sending card data to a third party may affect PCI scope and must be enabled deliberately.",
2950
+ { received: config.allowHttpClient, expected: true }
2951
+ );
2952
+ }
2953
+ validateDestinationRequest({ url, method, headers: options2 == null ? void 0 : options2.headers });
2954
+ log2("client invoked", { method, hasBody: body !== void 0 });
2955
+ if (!coordinatorClient) {
2956
+ log2("Waiting for coordinator initialization");
2957
+ await initializeCoordinator();
2958
+ }
2959
+ if (!coordinatorClient) {
2960
+ throw CoordinatorError("Coordinator failed to initialize");
2961
+ }
2962
+ const resolvedBody = body !== void 0 ? resolveElementReferences(body) : void 0;
2963
+ try {
2964
+ const result = await coordinatorClient.sendRequest(
2965
+ "CMD_HTTP_CLIENT",
2966
+ {
2967
+ method,
2968
+ url,
2969
+ headers: options2 == null ? void 0 : options2.headers,
2970
+ body: resolvedBody
2971
+ },
2972
+ { retries: 0 }
2973
+ );
2974
+ log2("client successful", { method });
2975
+ return result;
2976
+ } catch (error) {
2977
+ log2("client failed", { method, error: error.message });
2978
+ const transportError = error;
2979
+ if (transportError.code === "HTTP_CLIENT_ERROR") {
2980
+ throw HttpClientError(
2981
+ transportError.message.replace(/^\[HTTP_CLIENT_ERROR\] /, ""),
2982
+ transportError.status ?? -1,
2983
+ transportError.data,
2984
+ transportError.headers
2985
+ );
2986
+ }
2987
+ throw error;
2988
+ }
2989
+ });
2990
+ const withBody = (method) => (url, body, options2) => invokeClient(method, url, body, options2 ?? {});
2991
+ const withoutBody = (method) => (url, options2) => invokeClient(method, url, void 0, options2 ?? {});
2992
+ return {
2993
+ get: withoutBody("GET"),
2994
+ post: withBody("POST"),
2995
+ put: withBody("PUT"),
2996
+ patch: withBody("PATCH"),
2997
+ delete: withoutBody("DELETE")
2998
+ };
2999
+ })(),
2797
3000
  updateThemeMode: guardAsync(async (mode) => {
2798
3001
  log2("updateThemeMode called", {
2799
3002
  oldMode: currentThemeMode,
@@ -2960,8 +3163,8 @@ const BasisTheory = (apiKey, options = {}, queuedCommands) => {
2960
3163
  if (typeof window !== "undefined" && typeof document !== "undefined" && document.currentScript && !window.BasisTheory) {
2961
3164
  window.BasisTheory = BasisTheory;
2962
3165
  }
2963
- const version = "3.0.0";
2964
- const buildDate = "2026-08-13T19:30:19.198Z";
3166
+ const version = "3.1.0";
3167
+ const buildDate = "2026-09-21T19:33:43.220Z";
2965
3168
  if (typeof window !== "undefined") {
2966
3169
  window.__BasisTheorySDK__ = BasisTheory;
2967
3170
  }