├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src └── index.ts ├── test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JUNO Card Hash - NodeJS 2 | 3 | Esse projeto destina-se a pessoas que estão realizando a integração com a API de pagamentos [JUNO](https://juno.com.br/) e estão encontrando problemas para realizar o hash do cartão de crédito no Frontend. 4 | 5 | No nosso caso, tinhamos problema para realizar a criptografia dos dados do cartão, pela necessidade de utilizar uma chave pública em um ambiente ReactNative (Utilizando Expo). 6 | 7 | A plataforma não disponibiliza até o momento nenhuma forma de integração mobile que não seja usando as bibliotecas nativas. 8 | 9 | Dessa forma, fizemos o envio dos dados do cartão (requisição com SSL) para nosso backend Node e no backend criamos essa biblioteca que converte os dados do cartão em um Hash, para então enviar para a plataforma da JUNO. 10 | 11 | ## Instalação 12 | 13 | Instale o pacote através do comando: 14 | 15 | `npm install juno-nodejs --save` 16 | 17 | ## Utilização 18 | 19 | ``` 20 | const { JunoCardHash } = require('juno-nodejs'); 21 | 22 | const publicToken = ''; // Token público da api da JUNO 23 | const environment = 'sandbox'; // 'sandbox' || 'production' 24 | const cardData = { 25 | holderName: "José da Silva", 26 | cardNumber: "0000000000000000", 27 | securityCode: '000', 28 | expirationMonth: '12', 29 | expirationYear: '2025', 30 | }; 31 | 32 | const junoService = new JunoCardHash(publicToken, environment); 33 | 34 | junoService.getCardHash(cardData) 35 | .then(({ data }) => console.log(data)); // Hash 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juno-nodejs", 3 | "version": "1.0.13", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@peculiar/asn1-schema": { 8 | "version": "2.0.5", 9 | "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.5.tgz", 10 | "integrity": "sha512-VIKJjsgMkv+yyWx3C+D4xo6/NeCg0XFBgNlavtkxELijV+aKAq53du5KkOJbeZtm1nn9CinQKny2PqL8zCfpeA==", 11 | "requires": { 12 | "@types/asn1js": "^0.0.1", 13 | "asn1js": "^2.0.26", 14 | "pvtsutils": "^1.0.10", 15 | "tslib": "^1.11.1" 16 | } 17 | }, 18 | "@peculiar/json-schema": { 19 | "version": "1.1.10", 20 | "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.10.tgz", 21 | "integrity": "sha512-kbpnG9CkF1y6wwGkW7YtSA+yYK4X5uk4rAwsd1hxiaYE3Hkw2EsGlbGh/COkMLyFf+Fe830BoFiMSB3QnC/ItA==", 22 | "requires": { 23 | "tslib": "^1.11.1" 24 | } 25 | }, 26 | "@peculiar/webcrypto": { 27 | "version": "1.1.1", 28 | "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.1.1.tgz", 29 | "integrity": "sha512-Bu2XgOvzirnLcojZYs4KQ8hOLf2ETpa0NL6btQt5NgsAwctI6yVkzgYP+EcG7Mm579RBP+V0LM5rXyMlTVx23A==", 30 | "requires": { 31 | "@peculiar/asn1-schema": "^2.0.3", 32 | "@peculiar/json-schema": "^1.1.10", 33 | "pvtsutils": "^1.0.10", 34 | "tslib": "^1.11.2", 35 | "webcrypto-core": "^1.1.0" 36 | } 37 | }, 38 | "@types/asn1js": { 39 | "version": "0.0.1", 40 | "resolved": "https://registry.npmjs.org/@types/asn1js/-/asn1js-0.0.1.tgz", 41 | "integrity": "sha1-74uflwjLFjKhw6nNJ3F8qr55O8I=", 42 | "requires": { 43 | "@types/pvutils": "*" 44 | } 45 | }, 46 | "@types/axios": { 47 | "version": "0.14.0", 48 | "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", 49 | "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", 50 | "dev": true, 51 | "requires": { 52 | "axios": "*" 53 | } 54 | }, 55 | "@types/base-64": { 56 | "version": "0.1.3", 57 | "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-0.1.3.tgz", 58 | "integrity": "sha512-DJpw7RKNMXygZ0j2xe6ROBqiJUy7JWEItkzOPBzrT35HUWS7VLYyW9XJX8yCCvE2xg8QD7wesvVyXFg8AVHTMA==", 59 | "dev": true 60 | }, 61 | "@types/node": { 62 | "version": "14.0.12", 63 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.12.tgz", 64 | "integrity": "sha512-/sjzehvjkkpvLpYtN6/2dv5kg41otMGuHQUt9T2aiAuIfleCQRQHXXzF1eAw/qkZTj5Kcf4JSTf7EIizHocy6Q==", 65 | "dev": true 66 | }, 67 | "@types/pvutils": { 68 | "version": "0.0.2", 69 | "resolved": "https://registry.npmjs.org/@types/pvutils/-/pvutils-0.0.2.tgz", 70 | "integrity": "sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w==" 71 | }, 72 | "@types/qs": { 73 | "version": "6.9.3", 74 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", 75 | "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", 76 | "dev": true 77 | }, 78 | "@unimodules/core": { 79 | "version": "5.1.2", 80 | "resolved": "https://registry.npmjs.org/@unimodules/core/-/core-5.1.2.tgz", 81 | "integrity": "sha512-iCWEbzsNHqDfL6p8FyCGPnL2EW7vdgMJsNNSlWtM/gl8kePdqZMI7aOxTC4cdRS2xm0wzxuDBtpfJkzZsKINZg==", 82 | "optional": true, 83 | "requires": { 84 | "compare-versions": "^3.4.0" 85 | } 86 | }, 87 | "@unimodules/react-native-adapter": { 88 | "version": "5.2.0", 89 | "resolved": "https://registry.npmjs.org/@unimodules/react-native-adapter/-/react-native-adapter-5.2.0.tgz", 90 | "integrity": "sha512-S3HMEeQbV6xs7ORRcxXFGMk38DAnxqNcZG9T8JkX/KGY9ILUUqTS/e68+d849B6beEeglNMcOxyjwlqjykN+FA==", 91 | "optional": true, 92 | "requires": { 93 | "invariant": "^2.2.4", 94 | "lodash": "^4.5.0", 95 | "prop-types": "^15.6.1" 96 | } 97 | }, 98 | "asmcrypto.js": { 99 | "version": "0.22.0", 100 | "resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-0.22.0.tgz", 101 | "integrity": "sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA==" 102 | }, 103 | "asn1js": { 104 | "version": "2.0.26", 105 | "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz", 106 | "integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==", 107 | "requires": { 108 | "pvutils": "^1.0.17" 109 | } 110 | }, 111 | "axios": { 112 | "version": "0.19.2", 113 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", 114 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", 115 | "requires": { 116 | "follow-redirects": "1.5.10" 117 | } 118 | }, 119 | "b64-lite": { 120 | "version": "1.4.0", 121 | "resolved": "https://registry.npmjs.org/b64-lite/-/b64-lite-1.4.0.tgz", 122 | "integrity": "sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w==", 123 | "requires": { 124 | "base-64": "^0.1.0" 125 | } 126 | }, 127 | "b64u-lite": { 128 | "version": "1.1.0", 129 | "resolved": "https://registry.npmjs.org/b64u-lite/-/b64u-lite-1.1.0.tgz", 130 | "integrity": "sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A==", 131 | "requires": { 132 | "b64-lite": "^1.4.0" 133 | } 134 | }, 135 | "base-64": { 136 | "version": "0.1.0", 137 | "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", 138 | "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" 139 | }, 140 | "base64-js": { 141 | "version": "1.3.1", 142 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 143 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", 144 | "optional": true 145 | }, 146 | "compare-versions": { 147 | "version": "3.6.0", 148 | "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", 149 | "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", 150 | "optional": true 151 | }, 152 | "debug": { 153 | "version": "3.1.0", 154 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 155 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 156 | "requires": { 157 | "ms": "2.0.0" 158 | } 159 | }, 160 | "expo-random": { 161 | "version": "8.1.0", 162 | "resolved": "https://registry.npmjs.org/expo-random/-/expo-random-8.1.0.tgz", 163 | "integrity": "sha512-9n2gg83Hpg3ErkKu+a3FFOGmaPIxaHn6RuzjW24xFckdfmnrAKtbs1aU1aAcmoL1kXPvDeufRSEV/3lW93u6ug==", 164 | "optional": true, 165 | "requires": { 166 | "base64-js": "^1.3.0" 167 | } 168 | }, 169 | "follow-redirects": { 170 | "version": "1.5.10", 171 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 172 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 173 | "requires": { 174 | "debug": "=3.1.0" 175 | } 176 | }, 177 | "invariant": { 178 | "version": "2.2.4", 179 | "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", 180 | "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", 181 | "optional": true, 182 | "requires": { 183 | "loose-envify": "^1.0.0" 184 | } 185 | }, 186 | "isomorphic-webcrypto": { 187 | "version": "2.3.6", 188 | "resolved": "https://registry.npmjs.org/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.6.tgz", 189 | "integrity": "sha512-d1prB3b0UMWOao5DK3+O2Dr5ZJCakzB5Q+2kCWNkNuM9ln7VB8TSw2SwUjbnErzg7cgsYja+VPQaeBtXEojpew==", 190 | "requires": { 191 | "@peculiar/webcrypto": "^1.0.22", 192 | "@unimodules/core": "*", 193 | "@unimodules/react-native-adapter": "*", 194 | "asmcrypto.js": "^0.22.0", 195 | "b64-lite": "^1.3.1", 196 | "b64u-lite": "^1.0.1", 197 | "expo-random": "*", 198 | "msrcrypto": "^1.5.6", 199 | "react-native-securerandom": "^0.1.1", 200 | "str2buf": "^1.3.0", 201 | "webcrypto-shim": "^0.1.4" 202 | } 203 | }, 204 | "js-tokens": { 205 | "version": "4.0.0", 206 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 207 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 208 | "optional": true 209 | }, 210 | "lodash": { 211 | "version": "4.17.15", 212 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 213 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", 214 | "optional": true 215 | }, 216 | "loose-envify": { 217 | "version": "1.4.0", 218 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 219 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 220 | "optional": true, 221 | "requires": { 222 | "js-tokens": "^3.0.0 || ^4.0.0" 223 | } 224 | }, 225 | "ms": { 226 | "version": "2.0.0", 227 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 228 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 229 | }, 230 | "msrcrypto": { 231 | "version": "1.5.8", 232 | "resolved": "https://registry.npmjs.org/msrcrypto/-/msrcrypto-1.5.8.tgz", 233 | "integrity": "sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q==" 234 | }, 235 | "object-assign": { 236 | "version": "4.1.1", 237 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 238 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 239 | "optional": true 240 | }, 241 | "prop-types": { 242 | "version": "15.7.2", 243 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 244 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 245 | "optional": true, 246 | "requires": { 247 | "loose-envify": "^1.4.0", 248 | "object-assign": "^4.1.1", 249 | "react-is": "^16.8.1" 250 | } 251 | }, 252 | "pvtsutils": { 253 | "version": "1.0.10", 254 | "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.0.10.tgz", 255 | "integrity": "sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw==", 256 | "requires": { 257 | "tslib": "^1.10.0" 258 | } 259 | }, 260 | "pvutils": { 261 | "version": "1.0.17", 262 | "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz", 263 | "integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ==" 264 | }, 265 | "qs": { 266 | "version": "6.9.4", 267 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", 268 | "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" 269 | }, 270 | "react-is": { 271 | "version": "16.13.1", 272 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 273 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 274 | "optional": true 275 | }, 276 | "react-native-securerandom": { 277 | "version": "0.1.1", 278 | "resolved": "https://registry.npmjs.org/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz", 279 | "integrity": "sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA=", 280 | "optional": true, 281 | "requires": { 282 | "base64-js": "*" 283 | } 284 | }, 285 | "str2buf": { 286 | "version": "1.3.0", 287 | "resolved": "https://registry.npmjs.org/str2buf/-/str2buf-1.3.0.tgz", 288 | "integrity": "sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==" 289 | }, 290 | "tslib": { 291 | "version": "1.13.0", 292 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", 293 | "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" 294 | }, 295 | "typescript": { 296 | "version": "3.9.5", 297 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", 298 | "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", 299 | "dev": true 300 | }, 301 | "webcrypto-core": { 302 | "version": "1.1.1", 303 | "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.1.1.tgz", 304 | "integrity": "sha512-xK61sFRUyZdSAJG7+bJox36+Tnhxw1PaMbmrLLp30HNTJ4mffqsY2jUMlmGq6OOoej3WO/SsH5serzlzBMZ+jg==", 305 | "requires": { 306 | "@peculiar/asn1-schema": "^2.0.1", 307 | "@peculiar/json-schema": "^1.1.10", 308 | "asn1js": "^2.0.26", 309 | "pvtsutils": "^1.0.10", 310 | "tslib": "^1.11.2" 311 | } 312 | }, 313 | "webcrypto-shim": { 314 | "version": "0.1.5", 315 | "resolved": "https://registry.npmjs.org/webcrypto-shim/-/webcrypto-shim-0.1.5.tgz", 316 | "integrity": "sha512-mE+E00gulvbLjHaAwl0kph60oOLQRsKyivEFgV9DMM/3Y05F1vZvGq12hAcNzHRnYxyEOABBT/XMtwGSg5xA7A==" 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juno-nodejs", 3 | "version": "1.0.14", 4 | "description": "Esse projeto destina-se a pessoas que estão realizando a integração com a API de pagamentos [JUNO](https://juno.com.br/) e estão encontrando problemas para realizar o hash do cartão de crédito no Frontend.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "tsc -w", 9 | "prepublish": "npm run build" 10 | }, 11 | "author": "Murilo Campaner", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^0.19.2", 15 | "base-64": "^0.1.0", 16 | "isomorphic-webcrypto": "^2.3.6", 17 | "qs": "^6.9.4" 18 | }, 19 | "devDependencies": { 20 | "@types/axios": "^0.14.0", 21 | "@types/base-64": "^0.1.3", 22 | "@types/node": "^14.0.12", 23 | "@types/qs": "^6.9.3", 24 | "typescript": "^3.9.5" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/murilo-campaner/juno-nodejs.git" 29 | }, 30 | "keywords": [ 31 | "juno", 32 | "integracao", 33 | "api", 34 | "nodejs", 35 | "react-native", 36 | "cardhash", 37 | "card", 38 | "hash", 39 | "pagamentos" 40 | ], 41 | "bugs": { 42 | "url": "https://github.com/murilo-campaner/juno-nodejs/issues" 43 | }, 44 | "homepage": "https://github.com/murilo-campaner/juno-nodejs#readme" 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const crypto = require("isomorphic-webcrypto"); 2 | 3 | import { decode } from "base-64"; 4 | import * as qs from "qs"; 5 | import axios, { AxiosInstance } from "axios"; 6 | 7 | const ENVIRONMENT = { 8 | SANDBOX: "sandbox", 9 | PRODUCTION: "production", 10 | }; 11 | 12 | type ENV = "sandbox" | "production"; 13 | 14 | interface iCardData { 15 | holderName: string; 16 | cardNumber: string; 17 | securityCode: string; 18 | expirationMonth: string; 19 | expirationYear: string; 20 | } 21 | 22 | class JunoCardHash { 23 | publicToken: string; 24 | environment: ENV; 25 | axios: AxiosInstance; 26 | constructor(publicToken: string, environment: ENV = "sandbox") { 27 | this.publicToken = publicToken; 28 | this.environment = environment; 29 | this.axios = this._configureAxios(this.environment); 30 | } 31 | 32 | static getAlgorithm() { 33 | return { 34 | name: "RSA-OAEP", 35 | hash: { name: "SHA-256" }, 36 | }; 37 | } 38 | 39 | async getCardHash(cardData: iCardData) { 40 | const publicKey = await this._fetchPublicKey(); 41 | const binaryKey = this._getBinaryKey(publicKey); 42 | const encriptedPublicKey = await this._importKey(binaryKey); 43 | 44 | const cardBuffer = this._str2ab(JSON.stringify(cardData)); 45 | const encryptedCard = await this._encryptCardData( 46 | encriptedPublicKey, 47 | cardBuffer 48 | ); 49 | 50 | const result = await this._fetchCardHash(encryptedCard); 51 | 52 | if (!result.data) { 53 | throw new Error("Não foi possível gerar o hash do cartão"); 54 | } 55 | 56 | return result.data; 57 | } 58 | 59 | async _fetchPublicKey(): Promise { 60 | const params = qs.stringify({ publicToken: this.publicToken }); 61 | const ENDPOINT = `/get-public-encryption-key.json?${params}`; 62 | try { 63 | const { data } = await this.axios.post(ENDPOINT); 64 | return data.replace(/(\r\n|\n|\r)/gm, ""); // Remove line breaks 65 | } catch (error) { 66 | throw new Error( 67 | error.response.message || 68 | "Erro ao gerar a chave pública na API de pagamentos" 69 | ); 70 | } 71 | } 72 | 73 | _fetchCardHash(encryptedCard: string | void) { 74 | const params = qs.stringify({ 75 | publicToken: this.publicToken, 76 | encryptedData: encryptedCard, 77 | }); 78 | const ENDPOINT = `/get-credit-card-hash.json?${params}`; 79 | return this.axios.post(ENDPOINT); 80 | } 81 | 82 | _getBinaryKey(encodedKey: string) { 83 | const decodedKey = decode(encodedKey); // Decode base 64 84 | const binaryKey = this._str2ab(decodedKey); // Transform into an ArrayBuffer 85 | return binaryKey; 86 | } 87 | 88 | _importKey(binaryKey: JsonWebKey | ArrayBuffer): Promise { 89 | const algorithm = JunoCardHash.getAlgorithm(); 90 | 91 | return new Promise((resolve, reject) => 92 | crypto.subtle 93 | .importKey("spki", binaryKey, algorithm, false, ["encrypt"]) 94 | .then(resolve, reject) 95 | ); 96 | } 97 | 98 | _encryptCardData( 99 | publicKey: CryptoKey, 100 | encodedCardData: ArrayBuffer 101 | ): Promise { 102 | const algorithm = JunoCardHash.getAlgorithm(); 103 | return new Promise((resolve, reject) => 104 | crypto.subtle 105 | .encrypt(algorithm, publicKey, encodedCardData) 106 | .then((data: ArrayBuffer) => this._encodeAb(data), reject) 107 | .then((encoded: string) => resolve(encoded)) 108 | ); 109 | } 110 | 111 | _str2ab(str: string) { 112 | const buf = new ArrayBuffer(str.length); 113 | const bufView = new Uint8Array(buf); 114 | for (let i = 0, strLen = str.length; i < strLen; i++) { 115 | bufView[i] = str.charCodeAt(i); 116 | } 117 | return buf; 118 | } 119 | 120 | _encodeAb(arrayBuffer: ArrayBuffer) { 121 | let base64 = ""; 122 | const encodings = 123 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 124 | 125 | const bytes = new Uint8Array(arrayBuffer); 126 | const { byteLength } = bytes; 127 | const byteRemainder = byteLength % 3; 128 | const mainLength = byteLength - byteRemainder; 129 | 130 | let a; 131 | let b; 132 | let c; 133 | let d; 134 | let chunk; 135 | 136 | // Main loop deals with bytes in chunks of 3 137 | for (let i = 0; i < mainLength; i += 3) { 138 | // Combine the three bytes into a single integer 139 | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; 140 | 141 | // Use bitmasks to extract 6-bit segments from the triplet 142 | a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 143 | b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 144 | c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 145 | d = chunk & 63; // 63 = 2^6 - 1 146 | 147 | // Convert the raw binary segments to the appropriate ASCII encoding 148 | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; 149 | } 150 | 151 | // Deal with the remaining bytes and padding 152 | if (byteRemainder == 1) { 153 | chunk = bytes[mainLength]; 154 | 155 | a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 156 | 157 | // Set the 4 least significant bits to zero 158 | b = (chunk & 3) << 4; // 3 = 2^2 - 1 159 | 160 | base64 += `${encodings[a] + encodings[b]}==`; 161 | } else if (byteRemainder == 2) { 162 | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; 163 | 164 | a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 165 | b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 166 | 167 | // Set the 2 least significant bits to zero 168 | c = (chunk & 15) << 2; // 15 = 2^4 - 1 169 | 170 | base64 += `${encodings[a] + encodings[b] + encodings[c]}=`; 171 | } 172 | 173 | return base64; 174 | } 175 | 176 | _configureAxios(environment: ENV) { 177 | const baseURL = 178 | environment === ENVIRONMENT.SANDBOX 179 | ? "https://sandbox.boletobancario.com/boletofacil/integration/api" 180 | : "https://www.boletobancario.com/boletofacil/integration/api"; 181 | 182 | const instance = axios.create({ 183 | baseURL, 184 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 185 | }); 186 | 187 | instance.interceptors.response.use(({ data }) => data); 188 | 189 | return instance; 190 | } 191 | } 192 | 193 | export { JunoCardHash }; 194 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { JunoCardHash } = require("./lib/index"); 2 | 3 | const juno = new JunoCardHash( 4 | "PUBLIC_TOKEN", 5 | "sandbox" 6 | ); 7 | 8 | juno 9 | .getCardHash({ 10 | holderName: "José da Silva", 11 | cardNumber: "5253286010447710", 12 | securityCode: "172", 13 | expirationMonth: "09", 14 | expirationYear: "2021", 15 | }) 16 | .then((result) => { 17 | console.log(result); 18 | }) 19 | .catch((err) => { 20 | console.log(err.message); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules", "**/__tests__/*"] 11 | } 12 | --------------------------------------------------------------------------------