├── .prettierignore ├── .eslintignore ├── doc ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ └── OpenSans-LightItalic-webfont.woff ├── .template.hbs ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── lang-css.js │ │ ├── Apache-License-2.0.txt │ │ └── prettify.js ├── styles │ ├── prettify-jsdoc.css │ ├── prettify-tomorrow.css │ └── jsdoc-default.css ├── lib_smartcar-error.js.html ├── lib_util.js.html ├── SmartcarService.html ├── lib_smartcar-service.js.html ├── index.html ├── lib_auth-client.js.html └── index.js.html ├── .eslintrc.js ├── .docs.js ├── .nycrc.json ├── lib ├── config.json ├── smartcar-error.js ├── util.js ├── smartcar-service.js └── auth-client.js ├── .buddy ├── cd.yml └── ci.yml ├── .jsdoc.json ├── .gitignore ├── LICENSE.md ├── test ├── end-to-end │ ├── auth-client.js │ ├── util.js │ ├── helpers │ │ └── index.js │ └── index.js └── unit │ ├── lib │ ├── util.js │ ├── vehicle-coverage.js │ └── vehicle.js │ └── index.js ├── .travis.yml ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | ** 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | doc 3 | -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /doc/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /doc/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /doc/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /doc/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /doc/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcar/node-sdk/HEAD/doc/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'smartcar', 5 | parserOptions: { 6 | ecmaVersion: 2020, 7 | }, 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /.docs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Meta file for bootstrapping jsdoc 5 | */ 6 | 7 | /** 8 | * @global 9 | * @class Promise 10 | * @see http://bluebirdjs.com/docs/api-reference.html 11 | */ 12 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "lines": 93, 3 | "branches": 93, 4 | "functions": 93, 5 | "statements": 93, 6 | "all" : false, 7 | "cache": true, 8 | "reporter": [ 9 | "lcov", 10 | "text" 11 | ], 12 | "check-coverage": true 13 | } 14 | -------------------------------------------------------------------------------- /doc/.template.hbs: -------------------------------------------------------------------------------- 1 | # Smartcar Node SDK 2 | 3 | Smartcar Node SDK documentation. 4 | 5 | {{#indexChildren ~}} 6 | * [{{{name}}}] 7 | {{~#if (regexp-test see ":") ~}} 8 | (#module_{{{name}}}) 9 | {{else ~}} 10 | (#{{{name}}}) 11 | {{/if}} 12 | {{/indexChildren}} 13 | 14 | {{>main}} -------------------------------------------------------------------------------- /lib/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": "https://auth.smartcar.com", 3 | "api": "https://api.smartcar.com", 4 | "vehicle": "https://vehicle.api.smartcar.com", 5 | "connect": "https://connect.smartcar.com", 6 | "management": "https://management.smartcar.com", 7 | "timeout": 310000, 8 | "version": "2.0" 9 | } 10 | -------------------------------------------------------------------------------- /.buddy/cd.yml: -------------------------------------------------------------------------------- 1 | - pipeline: cd 2 | name: CD 3 | events: 4 | - type: PUSH 5 | refs: 6 | - refs/heads/master 7 | fail_on_prepare_env_warning: true 8 | actions: 9 | - action: semantic release 10 | type: BUILD 11 | docker_image_name: library/node 12 | docker_image_tag: 14 13 | execute_commands: 14 | - npm ci 15 | - npx semantic-release 16 | volume_mappings: 17 | - /:/buddy/retryify 18 | shell: BASH 19 | 20 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "source": { 6 | "includePattern": ".+\\.js$", 7 | "excludePattern": "test|node_modules|coverage" 8 | }, 9 | "plugins": ["plugins/markdown"], 10 | "templates": { 11 | "cleverLinks": false, 12 | "monospaceLinks": false, 13 | "default": { 14 | "outputSourceFiles": true 15 | } 16 | }, 17 | "opts": { 18 | "destination": "./doc/", 19 | "recurse": true, 20 | "readme": "README.md" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /doc/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System generated files 2 | *.log 3 | *.lnk 4 | .DS_Store 5 | Thumbs.db 6 | ehthumbs.db 7 | Desktop.ini 8 | 9 | # Packages 10 | node_modules 11 | bower_components 12 | 13 | # Testing/Coverage/Lint 14 | out 15 | dist 16 | coverage 17 | .dockerignore 18 | .eslintcache 19 | .tern-project 20 | .nyc_output 21 | 22 | # Editors 23 | 24 | ## Vscode 25 | .vscode 26 | .settings 27 | tsconfig.json 28 | jsconfig.json 29 | 30 | ## Vim 31 | *~ 32 | tags 33 | .netrwhist 34 | Session.vim 35 | [._]s[a-w][a-z] 36 | [._]*.]s[a-w][a-z]] 37 | 38 | ## Sublime 39 | *.tmlanguage.cache 40 | *.tmPreferences.cache 41 | *.stTheme.cache 42 | *.sublime-workspace 43 | sftp-config.json 44 | Package Control.last-run 45 | Package Control.ca-list 46 | Package Control.ca-bundle 47 | Package Control.system-ca-bundle 48 | Package Control.cache/ 49 | Package Control.ca-certs/ 50 | bh_unicode_properties.cache 51 | GitHub.sublime-settings 52 | 53 | ## env 54 | .env 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Smartcar, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/end-to-end/auth-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const test = require('ava'); 5 | 6 | const smartcar = require('../../'); 7 | const {getAuthClientParams, runAuthFlow, DEFAULT_SCOPES} = require('./helpers'); 8 | 9 | // Share auth tokens across tests to reduce auth flow calls 10 | test.before(async(t) => { 11 | const client = new smartcar.AuthClient(getAuthClientParams()); 12 | const code = await runAuthFlow(client.getAuthUrl(DEFAULT_SCOPES)); 13 | const tokens = await client.exchangeCode(code); 14 | 15 | t.context.client = client; 16 | t.context.tokens = tokens; 17 | }); 18 | 19 | test('exchangeCode', (t) => { 20 | const access = t.context.tokens; 21 | 22 | t.deepEqual( 23 | _.xor(_.keys(access), [ 24 | 'accessToken', 25 | 'expiration', 26 | 'refreshExpiration', 27 | 'refreshToken', 28 | ]), 29 | [], 30 | ); 31 | }); 32 | 33 | test('exchangeRefreshToken', async(t) => { 34 | const client = t.context.client; 35 | const initialTokens = t.context.tokens; 36 | 37 | const exchagedTokens = await client.exchangeRefreshToken( 38 | initialTokens.refreshToken, 39 | ); 40 | 41 | t.deepEqual( 42 | _.xor(_.keys(exchagedTokens), [ 43 | 'accessToken', 44 | 'expiration', 45 | 'refreshExpiration', 46 | 'refreshToken', 47 | ]), 48 | [], 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | services: 4 | - xvfb 5 | 6 | addons: 7 | firefox: latest 8 | 9 | language: 10 | - node_js 11 | 12 | node_js: 13 | - "16" 14 | - "14" 15 | - "12" 16 | 17 | before_install: 18 | - npm install -g npm@8.3.0 19 | 20 | cache: 21 | directories: 22 | - "$HOME/.npm" 23 | - ".eslintcache" 24 | 25 | if: tag IS blank # do not build tags 26 | 27 | notifications: 28 | slack: 29 | secure: "CHS39tAvw6WiIN8cYSyE3QT2PVeFdrHszT/00kU3pb1BH5Ec5JYx5U7bMrVf3yq3GQC/wxBhXs4E119LwZM0n04CGUOshShqSkLeUf9RfWYlqx2Qw2+tT8sOi+OXCzE+yaG3Yt+TWFqKyC5t0A9jZX6cdJbcBaKX2wJiOyI/HiTpXHXrJgeeflxRe03KDpIfpmWgpMjnouZ6rKMnP30H+CG4Ya5uouM/Sv5flgJ+1VnZo/kB89hQ4CELr3bBfxSW4lCS1Tmg/z8w059D2nsn7wiMols3Qgw4FJu773K03fyLGoV4JshxA9lvnLt/Vy+azDNEBP5drQeQ7l8GMrLAPIEN8oGbuH9+TyYoxj0P38Kx4hzlW3owGs1U2+wCuYCq2b58oGTYonKnynFV4Pi8f94uBWd6ziIJoKhwx6MsJzKIt/6T91QWjWozOpF9uGy81ZfR3WHU/gnIyWDTJsLnB7nFA5z9V3/K3Orj5tlVr7iZbCUhA9v6XMYTpyyxjoMtBjVad2IztaWXZIZ97Xx7WBkGS9lvFZIqgHMYERb/On/4bEEXdPlzyJxwwlPBNvPv7enVAsYjPJJ58CQ42fuYkMYZlNcTGfYz9Nw/K64ocMidfdMKvdFD1w6Cw/U1HkQ0SZssy6yEb1BpzdBfMbsUDnh0OJffUpeGp402XOwUNT4=" 30 | 31 | install: 32 | - npm ci 33 | - firefox -headless & 34 | 35 | script: 36 | # check if the docs have been generated 37 | - npm run docs 38 | - test -z "$(git diff --name-only | grep '^doc/readme.md$')" 39 | 40 | - npm run cover 41 | 42 | after_success: 43 | - bash <(curl -s https://codecov.io/bash) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smartcar", 3 | "version": "0.0.0-semantic-release", 4 | "description": "nodejs sdk for the smartcar platform", 5 | "main": "index.js", 6 | "author": "Smartcar (https://smartcar.com)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/smartcar/node-sdk.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/smartcar/node-sdk/issues" 14 | }, 15 | "homepage": "https://github.com/smartcar/node-sdk#readme", 16 | "keywords": [ 17 | "smartcar", 18 | "connected car" 19 | ], 20 | "files": [ 21 | "lib", 22 | "index.js" 23 | ], 24 | "engines": { 25 | "node": ">=18.20.0", 26 | "npm": ">=8.3.0" 27 | }, 28 | "scripts": { 29 | "test": "npm run test:unit", 30 | "test:unit": "ava test/unit", 31 | "test:e2e": "ava --serial --timeout=120s test/end-to-end", 32 | "test:integration": "cross-env NOCK_OFF=true npm test", 33 | "posttest": "npm run lint -s", 34 | "lint": "eslint . --cache", 35 | "cover": "nyc npm run test -s", 36 | "jsdoc": "jsdoc -c .jsdoc.json .", 37 | "docs": "mkdir -p doc && jsdoc2md --example-lang js --template doc/.template.hbs --files .docs.js index.js lib/* | sed -e 's/[ \t]*$//' -e 's/\\[\\ '//g' -e 's/'\\ \\]//g' > doc/readme.md" 38 | }, 39 | "dependencies": { 40 | "lodash": "^4.17.5" 41 | }, 42 | "devDependencies": { 43 | "ava": "^4.0.1", 44 | "cross-env": "^7.0.3", 45 | "eslint": "^8.57.0", 46 | "eslint-config-smartcar": "^3.0.0", 47 | "geckodriver": "^3.2.0", 48 | "is-ci": "^3.0.1", 49 | "jsdoc-to-markdown": "^7.1.1", 50 | "nock": "^14.0.10", 51 | "nyc": "^17.1.0", 52 | "selenium-webdriver": "^4.1.1", 53 | "semantic-release": "^19.0.2", 54 | "sinon": "^15.0.3", 55 | "uuid": "^8.3.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /doc/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /doc/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /.buddy/ci.yml: -------------------------------------------------------------------------------- 1 | - pipeline: ci 2 | name: CI 3 | events: 4 | - type: PUSH 5 | refs: 6 | - refs/heads/* 7 | fail_on_prepare_env_warning: true 8 | actions: 9 | - action: Run tests - Node 20 10 | type: BUILD 11 | docker_image_name: library/node 12 | docker_image_tag: 20 13 | execute_commands: 14 | - "# Set Firefox environment variables to help with headless operation" 15 | - Xvfb :99 -screen 0 1280x1024x24 & 16 | - export DISPLAY=:99.0 17 | - export MOZ_HEADLESS=1 18 | - export MOZ_NO_REMOTE=1 19 | - "export PATH=${HOME}/firefox-latest/firefox:$PATH" 20 | - firefox --version 21 | - firefox -headless & 22 | - "" 23 | - npm ci 24 | - npm run cover 25 | - bash <(curl -s https://codecov.io/bash) 26 | setup_commands: 27 | - apt-get update 28 | - apt-get install -y \ 29 | - ' libgtk-3-0 \' 30 | - ' libasound2 \' 31 | - ' libdbus-glib-1-2 \' 32 | - ' libx11-xcb1 \' 33 | - ' libxt6 \' 34 | - ' libnss3 \' 35 | - ' libxtst6 \' 36 | - ' libxss1 \' 37 | - ' libpci3 \' 38 | - ' libatk1.0-0 \' 39 | - ' libatk-bridge2.0-0 \' 40 | - ' libcups2 \' 41 | - ' libdrm2 \' 42 | - ' libxcomposite1 \' 43 | - ' libxdamage1 \' 44 | - ' libxfixes3 \' 45 | - ' libxkbcommon0 \' 46 | - ' libxrandr2 \' 47 | - ' xvfb' 48 | - "" 49 | - "" 50 | - "# Download the latest Firefox" 51 | - wget -O /tmp/firefox-latest.tar.xz 'https://download.mozilla.org/?product=firefox-latest&lang=en-US&os=linux64' 52 | - "mkdir -p ${HOME}/firefox-latest" 53 | - "tar -xJf /tmp/firefox-latest.tar.xz -C ${HOME}/firefox-latest" 54 | shell: BASH 55 | - action: Run tests - Node 22 56 | type: BUILD 57 | docker_image_name: library/node 58 | docker_image_tag: 22 59 | execute_commands: 60 | - "# Set Firefox environment variables to help with headless operation" 61 | - Xvfb :99 -screen 0 1280x1024x24 & 62 | - export DISPLAY=:99.0 63 | - export MOZ_HEADLESS=1 64 | - export MOZ_NO_REMOTE=1 65 | - "export PATH=${HOME}/firefox-latest/firefox:$PATH" 66 | - firefox --version 67 | - firefox -headless & 68 | - "" 69 | - npm ci 70 | - npm run cover 71 | - bash <(curl -s https://codecov.io/bash) 72 | setup_commands: 73 | - apt-get update 74 | - apt-get install -y \ 75 | - ' libgtk-3-0 \' 76 | - ' libasound2 \' 77 | - ' libdbus-glib-1-2 \' 78 | - ' libx11-xcb1 \' 79 | - ' libxt6 \' 80 | - ' libnss3 \' 81 | - ' libxtst6 \' 82 | - ' libxss1 \' 83 | - ' libpci3 \' 84 | - ' libatk1.0-0 \' 85 | - ' libatk-bridge2.0-0 \' 86 | - ' libcups2 \' 87 | - ' libdrm2 \' 88 | - ' libxcomposite1 \' 89 | - ' libxdamage1 \' 90 | - ' libxfixes3 \' 91 | - ' libxkbcommon0 \' 92 | - ' libxrandr2 \' 93 | - ' xvfb' 94 | - "" 95 | - "" 96 | - "# Download the latest Firefox" 97 | - wget -O /tmp/firefox-latest.tar.xz 'https://download.mozilla.org/?product=firefox-latest&lang=en-US&os=linux64' 98 | - "mkdir -p ${HOME}/firefox-latest" 99 | - "tar -xJf /tmp/firefox-latest.tar.xz -C ${HOME}/firefox-latest" 100 | shell: BASH 101 | - action: Verify README/docs have been generated 102 | type: BUILD 103 | docker_image_name: library/node 104 | docker_image_tag: 20 105 | execute_commands: 106 | - npm run docs 107 | - test -z "$(git diff --name-only | grep '^doc/readme.md$')" 108 | - bash <(curl -s https://codecov.io/bash) 109 | shell: BASH 110 | -------------------------------------------------------------------------------- /lib/smartcar-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @exports SmartcarError */ 4 | 5 | /** 6 | * Class to handle all errors from Smartcar API 7 | * Please see our [error guides]{@link https://smartcar.com/docs} to see a list 8 | * of all the possible error types and codes of both v2.0 and v1.0 requests. 9 | * */ 10 | class SmartcarError extends Error { 11 | 12 | /** 13 | * @param {number} status - response status 14 | * @param {object} body - response body 15 | * @param {object} headers - response headers 16 | */ 17 | constructor(status, body, headers) { 18 | const fields = [ 19 | 'type', 'code', 'description', 'docURL', 'detail', 'suggestedUserMessage', 20 | ]; 21 | if (body.error) { 22 | body.type = body.error; 23 | } 24 | 25 | if (!body.description) { 26 | // `error_description` is for handling oauth. 27 | if (body.error_description) { 28 | body.description = body.error_description; 29 | } else if (body.message) { 30 | body.description = body.message; 31 | } else if (typeof body !== 'string') { 32 | body.description = 'Unknown error'; 33 | } 34 | } 35 | 36 | if (typeof body === 'string') { 37 | super(body); 38 | } else { 39 | super(`${body.type}:${body.code} - ${body.description}`); 40 | } 41 | 42 | if (headers['retry-after']) { 43 | this.retryAfter = headers['retry-after']; 44 | } 45 | 46 | this.statusCode = status; 47 | this.requestId = body.requestId || headers['sc-request-id']; 48 | if (typeof body.resolution === 'string') { 49 | this.resolution = { 50 | type: body.resolution, 51 | }; 52 | } else if (body.resolution !== null 53 | && typeof body.resolution === 'object') { 54 | this.resolution = body.resolution; 55 | } 56 | 57 | // Now dynamically set the remaining ones if passed 58 | fields.forEach((item) => { 59 | if (body[item]) { 60 | this[item] = body[item]; 61 | } 62 | }); 63 | 64 | this.name = 'SmartcarError'; 65 | } 66 | } 67 | 68 | /** 69 | * Legacy field from V1 error depicting a category/type/description 70 | * of the error. 71 | * @var {string} SmartcarError.error 72 | */ 73 | 74 | /** 75 | * Error message field inherited from StandardError 76 | * @var {string} SmartcarError.message 77 | */ 78 | 79 | /** 80 | * Description of meaning of the error. 81 | * @var {string} SmartcarError.description 82 | */ 83 | 84 | /** 85 | * Type of error 86 | * @var {string} SmartcarError.type 87 | */ 88 | 89 | /** 90 | * Error code 91 | * @var {string} SmartcarError.code 92 | */ 93 | 94 | /** 95 | * HTTP status code 96 | * @var {number} SmartcarError.statusCode 97 | */ 98 | 99 | /** 100 | * Unique identifier for request 101 | * @var {string} SmartcarError.requestId 102 | */ 103 | 104 | /** 105 | * @type {Object} 106 | * @typedef SmartcarError.Resolution 107 | * @property {String} type - Possible hint to fixing the issue 108 | * @property {String} url - A URL to help resolve the issue or resume the operation 109 | */ 110 | 111 | /** 112 | * Possible resolution for fixing the error 113 | * @var {SmartcarError.Resolution} SmartcarError.resolution 114 | */ 115 | 116 | /** 117 | * Reference to Smartcar documentation 118 | * @var {string} SmartcarError.docURL 119 | */ 120 | 121 | /** 122 | * Further detail about the error in form of array of objects 123 | * @memberof SmartcarError 124 | * 125 | * @var {object[]} SmartcarError.details 126 | */ 127 | 128 | module.exports = SmartcarError; 129 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {format} = require('util'); 4 | 5 | const config = require('./config.json'); 6 | const SmartcarError = require('./smartcar-error'); 7 | const {version} = require('../package.json'); 8 | const {env} = require('process'); 9 | 10 | const util = {}; 11 | 12 | util.USER_AGENT = format( 13 | 'Smartcar/%s (%s; %s) Node.js %s', 14 | version, 15 | process.platform, 16 | process.arch, 17 | process.version, 18 | ); 19 | 20 | /** 21 | * Format Access object and set expiration properties. 22 | * 23 | * @param {Access} access access object 24 | * @return {Access} 25 | */ 26 | util.formatAccess = function(access) { 27 | const expiresIn = access.expires_in * 1000; // normalize to ms 28 | const expiration = new Date(Date.now() + expiresIn); 29 | const dayMs = 24 * 60 * 60 * 1000; 30 | const refreshExpiration = new Date(Date.now() + (60 * dayMs)); 31 | return { 32 | accessToken: access.access_token, 33 | refreshToken: access.refresh_token, 34 | expiration, 35 | refreshExpiration, 36 | }; 37 | }; 38 | 39 | /** 40 | * Form an API request URI. 41 | * 42 | * @param {String} id vehicle identifier 43 | * @param {String} endpoint API endpoint 44 | * @return {String} API request URI 45 | */ 46 | util.getUrl = function(id, endpoint, version = config.version) { 47 | const origin = 48 | version === '3' 49 | ? util.getConfig('SMARTCAR_API_V3_ORIGIN') || config.vehicle 50 | : util.getConfig('SMARTCAR_API_ORIGIN') || config.api; 51 | let url = `${origin}/v${version}/vehicles`; 52 | 53 | if (id) { 54 | url += `/${id}`; 55 | } 56 | 57 | if (endpoint) { 58 | url += `/${endpoint}`; 59 | } 60 | 61 | return url; 62 | }; 63 | 64 | util.getOrThrowConfig = function(configName) { 65 | if (env[configName]) { 66 | return env[configName]; 67 | } 68 | 69 | throw new Error( 70 | `${configName} not set or passed as arguments`, 71 | ); 72 | }; 73 | 74 | util.getConfig = function(configName) { 75 | return env[configName]; 76 | }; 77 | 78 | util.getFlagsString = function(flags) { 79 | return Object.entries(flags) 80 | .map(([key, value]) => `${key}:${value}`).join(' '); 81 | }; 82 | 83 | util.handleRes = async function(res) { 84 | const rawBody = await res.text(); 85 | let parsedBody; 86 | try { 87 | parsedBody = JSON.parse(rawBody); 88 | } catch { 89 | parsedBody = rawBody; 90 | } 91 | 92 | const parsedHeaders = {}; 93 | for (const [key, value] of res.headers.entries()) { 94 | parsedHeaders[key.toLowerCase()] = value; 95 | } 96 | 97 | return { 98 | parsedBody, 99 | parsedHeaders, 100 | }; 101 | }; 102 | 103 | 104 | util.handleError = function({body, headers, res}) { 105 | 106 | const contentType = String(headers['content-type']); 107 | if (!contentType.toLowerCase().includes('application/json')) { 108 | // body would be a string in this case 109 | throw new SmartcarError(res.status, body, headers); 110 | } 111 | 112 | if (typeof body === 'string') { 113 | throw new SmartcarError( 114 | res.status, 115 | {message: body, type: 'SDK_ERROR'}, 116 | headers, 117 | ); 118 | } 119 | 120 | if (body.error || body.type) { 121 | throw new SmartcarError(res.status, body, headers); 122 | } else { 123 | throw new SmartcarError( 124 | res.status, 125 | {body, type: 'SDK_ERROR'}, 126 | headers, 127 | ); 128 | } 129 | }; 130 | 131 | /** 132 | * 133 | * Generate the token for vehicle management APIs using the amt. 134 | * 135 | * @method 136 | * @param {String} amt - Application Management Token 137 | * @param {String} username 138 | * @return {String} managementToken 139 | */ 140 | util.getManagementToken = function(amt, username = 'default') { 141 | const credentials = `${username}:${amt}`; 142 | return Buffer.from(credentials).toString('base64'); 143 | }; 144 | 145 | module.exports = util; 146 | -------------------------------------------------------------------------------- /test/end-to-end/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const smartcar = require('../../'); 6 | const SmartcarError = require('../../lib/smartcar-error'); 7 | const {getAuthClientParams, runAuthFlow} = require('./helpers'); 8 | 9 | const getVehicleObject = async function(email, version = '') { 10 | const client = new smartcar.AuthClient(getAuthClientParams()); 11 | const code = await runAuthFlow( 12 | client.getAuthUrl(['read_odometer'], {forcePrompt: true}), 13 | 'CHEVROLET', 14 | email, 15 | ); 16 | const {accessToken} = await client.exchangeCode(code); 17 | const {vehicles} = await smartcar.getVehicles(accessToken); 18 | 19 | return new smartcar.Vehicle(vehicles[0], accessToken, {version}); 20 | }; 21 | 22 | // Note: we skip the following 2 tests because the account associated with them 23 | // began throwing Smartcar Authentication errors. At this time, we have not diagnosed 24 | // exactly why this is happening, so we skip for now to move projects forward. 25 | test.skip('handleError-SmartcarError V2 resolution string', async function(t) { 26 | const description = 'The vehicle was unable to perform your request' 27 | + ' due to an unknown issue.'; 28 | 29 | const vehicle = await getVehicleObject( 30 | 'VEHICLE_STATE.UNKNOWN@smartcar.com', 31 | ); 32 | const error = await t.throwsAsync(vehicle.odometer()); 33 | 34 | t.true(error instanceof SmartcarError); 35 | t.is(error.statusCode, 409); 36 | t.is(error.resolution.type, 'RETRY_LATER'); 37 | t.is(error.docURL, 'https://smartcar.com/docs/errors/api-errors/vehicle-state-errors#unknown'); 38 | t.is(error.description, description); 39 | t.is(error.type, 'VEHICLE_STATE'); 40 | t.is(error.code, 'UNKNOWN'); 41 | t.is(error.requestId.length, 36); 42 | t.is(error.message, `VEHICLE_STATE:UNKNOWN - ${description}`); 43 | }); 44 | 45 | test.skip('handleError - SmartcarError V2 resolution null', async function(t) { 46 | const description = 'This vehicle is no longer associated with the user\'s ' 47 | + 'connected services account. Please prompt the user to re-add' 48 | + ' the vehicle to their account.'; 49 | 50 | const vehicle = await getVehicleObject( 51 | 'CONNECTED_SERVICES_ACCOUNT.VEHICLE_MISSING@smartcar.com', 52 | ); 53 | const error = await t.throwsAsync(vehicle.odometer()); 54 | 55 | t.true(error instanceof SmartcarError); 56 | t.is(error.statusCode, 400); 57 | t.is(error.resolution.type, null); 58 | t.is(error.docURL, 'https://smartcar.com/docs/errors/api-errors/connected-services-account-errors#vehicle-missing'); 59 | t.is(error.description, description); 60 | t.is(error.type, 'CONNECTED_SERVICES_ACCOUNT'); 61 | t.is(error.code, 'VEHICLE_MISSING'); 62 | t.is(error.requestId.length, 36); 63 | t.is(error.message, 64 | `CONNECTED_SERVICES_ACCOUNT:VEHICLE_MISSING - ${description}`, 65 | ); 66 | }); 67 | 68 | test.skip('handleError - SmartcarError V1 error', async function(t) { 69 | const vehicle = await getVehicleObject( 70 | 'smartcar@vs-000.vehicle-state-error.com', 71 | '1.0', 72 | ); 73 | const error = await t.throwsAsync(vehicle.odometer()); 74 | 75 | t.true(error instanceof SmartcarError); 76 | t.is(error.statusCode, 409); 77 | t.is(error.type, 'vehicle_state_error'); 78 | t.is(error.code, 'VS_000'); 79 | t.is(error.requestId.length, 36); 80 | const expectedMessage = 'vehicle_state_error:VS_000 - ' 81 | + 'Vehicle state cannot be determined.'; 82 | t.is(error.message, expectedMessage); 83 | }); 84 | 85 | test.skip('handleError - SmartcarError V2 code null', async function(t) { 86 | const description = 'Your application has insufficient permissions to access ' 87 | + 'the requested resource. Please prompt the user to re-authenticate' 88 | + ' using Smartcar Connect.'; 89 | 90 | const vehicle = await getVehicleObject(); 91 | const error = await t.throwsAsync(vehicle.location()); 92 | 93 | t.true(error instanceof SmartcarError); 94 | t.is(error.statusCode, 403); 95 | t.is(error.resolution.type, 'REAUTHENTICATE'); 96 | t.is(error.docURL, 'https://smartcar.com/docs/errors/api-errors/permission-errors#null'); 97 | t.is(error.description, description); 98 | t.is(error.type, 'PERMISSION'); 99 | t.is(error.code, undefined); 100 | t.is(error.requestId.length, 36); 101 | t.is(error.message, `PERMISSION:null - ${description}`); 102 | }); 103 | -------------------------------------------------------------------------------- /test/end-to-end/helpers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chrome = require('selenium-webdriver/chrome'); 4 | const firefox = require('selenium-webdriver/firefox'); 5 | const isCI = require('is-ci'); 6 | const {v4: uuid} = require('uuid'); 7 | const {Builder, By, until} = require('selenium-webdriver'); 8 | const {URL} = require('url'); 9 | 10 | const helpers = {}; 11 | 12 | /* eslint-disable no-process-env */ 13 | const HEADLESS = isCI || process.env.HEADLESS; 14 | const SELENIUM_REMOTE_URL = process.env.SELENIUM_REMOTE_URL; 15 | const SELENIUM_BROWSER = process.env.SELENIUM_BROWSER || 'firefox'; 16 | const BROWSER = process.env.BROWSER || SELENIUM_BROWSER; 17 | const CLIENT_ID = process.env.E2E_SMARTCAR_CLIENT_ID; 18 | const CLIENT_SECRET = process.env.E2E_SMARTCAR_CLIENT_SECRET; 19 | 20 | /* eslint-enable */ 21 | 22 | if (!CLIENT_ID || !CLIENT_SECRET) { 23 | throw new Error( 24 | // eslint-disable-next-line max-len 25 | '"E2E_SMARTCAR_CLIENT_ID" and "E2E_SMARTCAR_CLIENT_SECRET" environment variables must be set', 26 | ); 27 | } 28 | 29 | helpers.DEFAULT_SCOPES = [ 30 | 'required:read_vehicle_info', 31 | 'required:read_location', 32 | 'required:read_odometer', 33 | 'required:control_security', 34 | 'required:read_vin', 35 | 'required:read_fuel', 36 | 'required:read_battery', 37 | 'required:read_charge', 38 | 'required:read_engine_oil', 39 | 'required:read_tires', 40 | 'required:read_user_profile', 41 | 'required:read_diagnostics', 42 | ]; 43 | 44 | helpers.getAuthClientParams = () => ({ 45 | clientId: CLIENT_ID, 46 | clientSecret: CLIENT_SECRET, 47 | redirectUri: 'https://example.com/auth', 48 | mode: 'test', 49 | }); 50 | 51 | const getCodeFromUri = function(uri) { 52 | const {searchParams} = new URL(uri); 53 | const code = searchParams.get('code'); 54 | 55 | if (code) { 56 | return code; 57 | } else { 58 | throw new Error( 59 | `Did not get code in url! Query string: ${searchParams.get('error')}`, 60 | ); 61 | } 62 | }; 63 | 64 | const getDriver = function() { 65 | const builder = new Builder(); 66 | 67 | if (SELENIUM_REMOTE_URL) { 68 | return builder.build(); 69 | } 70 | 71 | const firefoxOptions = new firefox.Options(); 72 | const chromeOptions = new chrome.Options() 73 | .addArguments('disable-infobars') 74 | // eslint-disable-next-line camelcase 75 | .setUserPreferences({credential_enable_service: false}); 76 | 77 | if (HEADLESS) { 78 | firefoxOptions.headless(); 79 | chromeOptions.addArguments('headless'); 80 | } 81 | 82 | return builder 83 | .setChromeOptions(chromeOptions) 84 | .setFirefoxOptions(firefoxOptions) 85 | .forBrowser(BROWSER) 86 | .build(); 87 | }; 88 | 89 | helpers.runAuthFlow = async function( 90 | authUrl, 91 | brand = 'CHEVROLET', 92 | email = '', 93 | ) { 94 | const driver = getDriver(); 95 | 96 | await driver.get(authUrl); 97 | 98 | // Preamble 99 | const continueButton = await driver.wait( 100 | until.elementLocated( 101 | By.css('button#continue-button'), 102 | ), 103 | ); 104 | await continueButton.click(); 105 | 106 | // Filter 107 | if (brand === 'KIA') { 108 | const brandInput = await driver.wait( 109 | until.elementLocated( 110 | By.css('input[id=brand-search]'), 111 | ), 112 | ); 113 | await brandInput.sendKeys(brand); 114 | } 115 | 116 | // Brand Selector 117 | const brandButton = await driver.wait( 118 | until.elementLocated( 119 | By.css(`button#${brand.toUpperCase()}.brand-list-item`), 120 | ), 121 | ); 122 | brandButton.click(); 123 | 124 | // Login 125 | const signInButton = await driver.wait( 126 | until.elementLocated( 127 | By.id('sign-in-button'), 128 | ), 129 | ); 130 | email = email || `${uuid()}@email.com`; 131 | await driver.findElement(By.css('input[id=username]')).sendKeys(email); 132 | await driver.findElement(By.css('input[id=password')).sendKeys('password'); 133 | await signInButton.click(); 134 | 135 | // Permissions 136 | await driver.sleep(5000); 137 | await driver.wait(until.elementLocated(By.css('.page-content'))); 138 | await driver.findElement(By.css('button[id=approval-button]')).click(); 139 | 140 | await driver.wait(until.urlContains('example.com')); 141 | 142 | const url = await driver.getCurrentUrl(); 143 | await driver.quit(); 144 | 145 | return getCodeFromUri(url); 146 | }; 147 | 148 | module.exports = helpers; 149 | -------------------------------------------------------------------------------- /doc/lib_smartcar-error.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: lib/smartcar-error.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: lib/smartcar-error.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | /** @exports SmartcarError */
 32 | 
 33 | /**
 34 |  * Class to handle all errors from Smartcar API
 35 |  * Please see our [error guides]{@link https://smartcar.com/docs} to see a list
 36 |  * of all the possible error types and codes of both v2.0 and v1.0 requests.
 37 |  * */
 38 | class SmartcarError extends Error {
 39 | 
 40 |   /**
 41 |    * @param {number} status - response status
 42 |    * @param {object} body - response body
 43 |    * @param {object} headers - response headers
 44 |    */
 45 |   constructor(status, body, headers) {
 46 |     const fields = [
 47 |       'type', 'code', 'description', 'docURL', 'detail', 'suggestedUserMessage',
 48 |     ];
 49 |     if (body.error) {
 50 |       body.type = body.error;
 51 |     }
 52 | 
 53 |     if (!body.description) {
 54 |       // `error_description` is for handling oauth.
 55 |       if (body.error_description) {
 56 |         body.description = body.error_description;
 57 |       } else if (body.message) {
 58 |         body.description = body.message;
 59 |       } else if (typeof body !== 'string') {
 60 |         body.description = 'Unknown error';
 61 |       }
 62 |     }
 63 | 
 64 |     if (typeof body === 'string') {
 65 |       super(body);
 66 |     } else {
 67 |       super(`${body.type}:${body.code} - ${body.description}`);
 68 |     }
 69 | 
 70 |     if (headers['retry-after']) {
 71 |       this.retryAfter = headers['retry-after'];
 72 |     }
 73 | 
 74 |     this.statusCode = status;
 75 |     this.requestId = body.requestId || headers['sc-request-id'];
 76 |     if (typeof body.resolution === 'string') {
 77 |       this.resolution = {
 78 |         type: body.resolution,
 79 |       };
 80 |     } else if (body.resolution !== null
 81 |         && typeof body.resolution === 'object') {
 82 |       this.resolution = body.resolution;
 83 |     }
 84 | 
 85 |     // Now dynamically set the remaining ones if passed
 86 |     fields.forEach((item) => {
 87 |       if (body[item]) {
 88 |         this[item] = body[item];
 89 |       }
 90 |     });
 91 | 
 92 |     this.name = 'SmartcarError';
 93 |   }
 94 | }
 95 | 
 96 | /**
 97 |  * Legacy field from V1 error depicting a category/type/description
 98 |  * of the error.
 99 |  * @var {string} SmartcarError.error
100 |  */
101 | 
102 | /**
103 |  * Error message field inherited from StandardError
104 |  * @var {string} SmartcarError.message
105 |  */
106 | 
107 | /**
108 |  * Description of meaning of the error.
109 |  * @var {string} SmartcarError.description
110 |  */
111 | 
112 | /**
113 |  * Type of error
114 |  * @var {string} SmartcarError.type
115 |  */
116 | 
117 | /**
118 |  * Error code
119 |  * @var {string} SmartcarError.code
120 |  */
121 | 
122 | /**
123 |  * HTTP status code
124 |  * @var {number} SmartcarError.statusCode
125 |  */
126 | 
127 | /**
128 |  * Unique identifier for request
129 |  * @var {string} SmartcarError.requestId
130 |  */
131 | 
132 | /**
133 |  * @type {Object}
134 |  * @typedef SmartcarError.Resolution
135 |  * @property {String} type - Possible hint to fixing the issue
136 |  * @property {String} url - A URL to help resolve the issue or resume the operation
137 |  */
138 | 
139 | /**
140 |  * Possible resolution for fixing the error
141 |  * @var {SmartcarError.Resolution} SmartcarError.resolution
142 |  */
143 | 
144 | /**
145 |  * Reference to Smartcar documentation
146 |  * @var {string} SmartcarError.docURL
147 |  */
148 | 
149 | /**
150 |  * Further detail about the error in form of array of objects
151 |  * @memberof SmartcarError
152 |  *
153 |  * @var {object[]} SmartcarError.details
154 |  */
155 | 
156 | module.exports = SmartcarError;
157 | 
158 |
159 |
160 | 161 | 162 | 163 | 164 |
165 | 166 | 169 | 170 |
171 | 172 |
173 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 174 |
175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /doc/lib_util.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: lib/util.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: lib/util.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | const {format} = require('util');
 32 | 
 33 | const config = require('./config.json');
 34 | const SmartcarError = require('./smartcar-error');
 35 | const {version} = require('../package.json');
 36 | const {env} = require('process');
 37 | 
 38 | const util = {};
 39 | 
 40 | util.USER_AGENT = format(
 41 |   'Smartcar/%s (%s; %s) Node.js %s',
 42 |   version,
 43 |   process.platform,
 44 |   process.arch,
 45 |   process.version,
 46 | );
 47 | 
 48 | /**
 49 |  * Format Access object and set expiration properties.
 50 |  *
 51 |  * @param {Access} access access object
 52 |  * @return {Access}
 53 |  */
 54 | util.formatAccess = function(access) {
 55 |   const expiresIn = access.expires_in * 1000; // normalize to ms
 56 |   const expiration = new Date(Date.now() + expiresIn);
 57 |   const dayMs = 24 * 60 * 60 * 1000;
 58 |   const refreshExpiration = new Date(Date.now() + (60 * dayMs));
 59 |   return {
 60 |     accessToken: access.access_token,
 61 |     refreshToken: access.refresh_token,
 62 |     expiration,
 63 |     refreshExpiration,
 64 |   };
 65 | };
 66 | 
 67 | /**
 68 |  * Form an API request URI.
 69 |  *
 70 |  * @param {String} id vehicle identifier
 71 |  * @param {String} endpoint API endpoint
 72 |  * @return {String} API request URI
 73 |  */
 74 | util.getUrl = function(id, endpoint, version = config.version) {
 75 |   const origin = util.getConfig('SMARTCAR_API_ORIGIN') || config.api;
 76 |   let url = `${origin}/v${version}/vehicles`;
 77 | 
 78 |   if (id) {
 79 |     url += `/${id}`;
 80 |   }
 81 | 
 82 |   if (endpoint) {
 83 |     url += `/${endpoint}`;
 84 |   }
 85 | 
 86 |   return url;
 87 | };
 88 | 
 89 | util.getOrThrowConfig = function(configName) {
 90 |   if (env[configName]) {
 91 |     return env[configName];
 92 |   }
 93 | 
 94 |   throw new Error(
 95 |     `${configName} not set or passed as arguments`,
 96 |   );
 97 | };
 98 | 
 99 | util.getConfig = function(configName) {
100 |   return env[configName];
101 | };
102 | 
103 | util.getFlagsString = function(flags) {
104 |   return Object.entries(flags)
105 |     .map(([key, value]) => `${key}:${value}`).join(' ');
106 | };
107 | 
108 | util.handleRes = async function(res) {
109 |   const rawBody = await res.text();
110 |   let parsedBody;
111 |   try {
112 |     parsedBody = JSON.parse(rawBody);
113 |   } catch {
114 |     parsedBody = rawBody;
115 |   }
116 | 
117 |   const parsedHeaders = {};
118 |   for (const [key, value] of res.headers.entries()) {
119 |     parsedHeaders[key.toLowerCase()] = value;
120 |   }
121 | 
122 |   return {
123 |     parsedBody,
124 |     parsedHeaders,
125 |   };
126 | };
127 | 
128 | 
129 | util.handleError = function({body, headers, res}) {
130 | 
131 |   const contentType = String(headers['content-type']);
132 |   if (!contentType.toLowerCase().includes('application/json')) {
133 |     // body would be a string in this case
134 |     throw new SmartcarError(res.status, body, headers);
135 |   }
136 | 
137 |   if (typeof body === 'string') {
138 |     throw new SmartcarError(
139 |       res.status,
140 |       {message: body, type: 'SDK_ERROR'},
141 |       headers,
142 |     );
143 |   }
144 | 
145 |   if (body.error || body.type) {
146 |     throw new SmartcarError(res.status, body, headers);
147 |   } else {
148 |     throw new SmartcarError(
149 |       res.status,
150 |       {body, type: 'SDK_ERROR'},
151 |       headers,
152 |     );
153 |   }
154 | };
155 | 
156 | /**
157 |  *
158 |  * Generate the token for vehicle management APIs using the amt.
159 |  *
160 |  * @method
161 |  * @param {String} amt - Application Management Token
162 |  * @param {String} username
163 |  * @return {String} managementToken
164 |  */
165 | util.getManagementToken = function(amt, username = 'default') {
166 |   const credentials = `${username}:${amt}`;
167 |   return Buffer.from(credentials).toString('base64');
168 | };
169 | 
170 | module.exports = util;
171 | 
172 |
173 |
174 | 175 | 176 | 177 | 178 |
179 | 180 | 183 | 184 |
185 | 186 |
187 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 188 |
189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /test/end-to-end/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const test = require('ava'); 5 | const {env} = require('process'); 6 | 7 | const smartcar = require('../../'); 8 | const util = require('../../lib/util'); 9 | const {getAuthClientParams, runAuthFlow, DEFAULT_SCOPES} = require('./helpers'); 10 | 11 | test.before(async(t) => { 12 | const client = new smartcar.AuthClient(getAuthClientParams()); 13 | const code = await runAuthFlow(client.getAuthUrl(DEFAULT_SCOPES)); 14 | const {accessToken} = await client.exchangeCode(code); 15 | const {vehicles} = await smartcar.getVehicles(accessToken); 16 | const {id: userId} = await smartcar.getUser(accessToken); 17 | t.context.userId = userId; 18 | t.context.accessToken = accessToken; 19 | t.context.connectedVehicles = vehicles; 20 | 21 | const v3TestVehicleId = 'tst2e255-d3c8-4f90-9fec-e6e68b98e9cb'; 22 | const v3TestToken = 'test-data-token'; 23 | const v3TestVehicle = new smartcar.Vehicle(v3TestVehicleId, v3TestToken); 24 | const v3TestOrigin = 'https://vehicle.api.smartcar.com'; 25 | 26 | t.context.v3TestVehicle = v3TestVehicle; 27 | t.context.v3TestOrigin = v3TestOrigin; 28 | }); 29 | 30 | test('getVehicles', async(t) => { 31 | const response = await smartcar.getVehicles(t.context.accessToken); 32 | t.deepEqual( 33 | _.xor(_.keys(response), [ 34 | 'vehicles', 35 | 'paging', 36 | 'meta', 37 | ]), 38 | [], 39 | ); 40 | t.deepEqual(response.paging.offset, 0); 41 | response.vehicles.forEach((vehicleId) => { 42 | t.deepEqual(vehicleId.length, 36); 43 | }); 44 | t.deepEqual(response.meta.requestId.length, 36); 45 | }); 46 | 47 | test('getVehicle - v3', async(t) => { 48 | 49 | env.SMARTCAR_API_V3_ORIGIN = t.context.v3TestOrigin; 50 | const {v3TestVehicle} = t.context; 51 | const token = v3TestVehicle.token; 52 | const vehicleId = v3TestVehicle.id; 53 | const response = await smartcar.getVehicle(token, vehicleId); 54 | 55 | t.is(response.body.id, vehicleId); 56 | delete env.SMARTCAR_API_V3_ORIGIN; 57 | 58 | }); 59 | 60 | test('getUser', async(t) => { 61 | const response = await smartcar.getUser(t.context.accessToken); 62 | t.deepEqual( 63 | _.xor(_.keys(response), [ 64 | 'id', 65 | 'meta', 66 | ]), 67 | [], 68 | ); 69 | 70 | t.deepEqual(response.id.length, 36); 71 | t.deepEqual(response.meta.requestId.length, 36); 72 | }); 73 | 74 | test('getCompatibility', async(t) => { 75 | const {clientId, clientSecret} = getAuthClientParams(); 76 | const teslaVin = '5YJXCDE22HF068739'; 77 | const audiVin = 'WAUAFAFL1GN014882'; 78 | 79 | const scope = ['read_odometer', 'read_location']; 80 | 81 | const teslaComp = await smartcar.getCompatibility( 82 | teslaVin, 83 | scope, 84 | null, 85 | {clientId, clientSecret}, 86 | ); 87 | const audiComp = await smartcar.getCompatibility( 88 | audiVin, 89 | scope, 90 | null, 91 | {clientId, clientSecret}, 92 | ); 93 | [teslaComp, audiComp].forEach((response) => { 94 | t.deepEqual( 95 | _.xor(_.keys(response), [ 96 | 'compatible', 97 | 'reason', 98 | 'capabilities', 99 | 'meta', 100 | ]), 101 | [], 102 | ); 103 | t.truthy(response.compatible); 104 | t.deepEqual(response.meta.requestId.length, 36); 105 | t.deepEqual(response.capabilities.length, 2); 106 | }); 107 | t.truthy(_.every(audiComp.capabilities, ['capable', false])); 108 | t.truthy(_.every(teslaComp.capabilities, ['capable', true])); 109 | }); 110 | 111 | test.serial('getCompatibilityMatrix', async(t) => { 112 | const {clientId, clientSecret} = getAuthClientParams(); 113 | const response = await smartcar.getCompatibilityMatrix('US', { 114 | scope: ['read_battery', 'read_charge'], 115 | make: 'NISSAN', 116 | type: 'BEV', 117 | clientId, 118 | clientSecret, 119 | }); 120 | t.truthy(response.NISSAN); 121 | response.NISSAN.forEach((vehicle) => { 122 | t.deepEqual( 123 | _.xor(_.keys(vehicle), [ 124 | 'model', 125 | 'type', 126 | 'startYear', 127 | 'endYear', 128 | 'endpoints', 129 | 'permissions', 130 | ]), 131 | [], 132 | ); 133 | t.is(vehicle.type, 'BEV'); 134 | t.truthy(vehicle.permissions.includes('read_battery')); 135 | t.truthy(vehicle.permissions.includes('read_charge')); 136 | }); 137 | }); 138 | 139 | test.serial('getConnections', async(t) => { 140 | const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT'); 141 | const res = await smartcar.getConnections(amt); 142 | t.truthy(res.connections[0].userId); 143 | t.truthy(res.connections[0].vehicleId); 144 | t.truthy(res.connections[0].connectedAt); 145 | }); 146 | 147 | test.serial('getConnections - by vehicleId', async(t) => { 148 | const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT'); 149 | const testVehicleId = t.context.connectedVehicles[0]; 150 | const res = await smartcar.getConnections(amt, {vehicleId: testVehicleId}); 151 | t.is(res.connections.length, 1); 152 | t.is(res.connections[0].vehicleId, testVehicleId); 153 | }); 154 | 155 | test.serial('getConnections - by userId', async(t) => { 156 | const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT'); 157 | const res = await smartcar.getConnections(amt, {userId: t.context.userId}); 158 | t.is(res.connections.length, t.context.connectedVehicles.length); 159 | for (const connection of res.connections) { 160 | t.is(connection.userId, t.context.userId); 161 | } 162 | }); 163 | 164 | test.serial('getConnections - by userId - limit 1', async(t) => { 165 | const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT'); 166 | const res = await smartcar.getConnections(amt, 167 | {userId: t.context.userId}, 168 | {limit: 1}, 169 | ); 170 | t.is(res.connections.length, t.context.connectedVehicles.length); 171 | }); 172 | 173 | test.serial('deleteConnections - by vehicleId', async(t) => { 174 | const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT'); 175 | const testVehicleId = t.context.connectedVehicles[0]; 176 | const res = await smartcar.deleteConnections(amt, {vehicleId: testVehicleId}); 177 | t.is(res.connections.length, 1); 178 | t.is(res.connections[0].vehicleId, testVehicleId); 179 | }); 180 | 181 | test.serial('deleteConnections - by userId', async(t) => { 182 | const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT'); 183 | const res = await smartcar.deleteConnections(amt, {userId: t.context.userId}); 184 | // to account for serial test above 185 | t.is(res.connections.length, t.context.connectedVehicles.length - 1); 186 | for (const connection of res.connections) { 187 | t.is(connection.userId, t.context.userId); 188 | } 189 | }); 190 | -------------------------------------------------------------------------------- /lib/smartcar-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const _ = require('lodash'); 5 | const SmartcarError = require('./smartcar-error'); 6 | const config = require('./config.json'); 7 | const util = require('./util'); 8 | const HEADER_TO_META_KEYS = { 9 | 'sc-data-age': 'dataAge', 10 | 'sc-unit-system': 'unitSystem', 11 | 'sc-request-id': 'requestId', 12 | 'sc-fetched-at': 'fetchedAt', 13 | }; 14 | 15 | 16 | const buildMeta = (headers) => { 17 | const lowerCaseHeaders = _.mapKeys( 18 | headers, 19 | (_, key) => key.toLocaleLowerCase(), 20 | ); 21 | const meta = {}; 22 | Object.entries(HEADER_TO_META_KEYS).forEach(([headerName, key]) => { 23 | if (lowerCaseHeaders[headerName]) { 24 | meta[key] = lowerCaseHeaders[headerName]; 25 | } 26 | }); 27 | if (meta.dataAge) { 28 | meta.dataAge = new Date(meta.dataAge); 29 | } 30 | if (meta.fetchedAt) { 31 | meta.fetchedAt = new Date(meta.fetchedAt); 32 | } 33 | 34 | return meta; 35 | }; 36 | 37 | const getNameFromPath = (path) => { 38 | // Using this constant for edge cases. 39 | // '/' should have a method name of 'attributes' 40 | // '/tires/pressure' should be tirePressure and NOT tiresPressure 41 | const BATCH_PATH_TO_ATTRIBUTE = { 42 | '/security': 'lockStatus', 43 | '/tires/pressure': 'tirePressure', 44 | '/diagnostics/system_status': 'diagnosticSystemStatus', 45 | '/diagnostics/dtcs': 'diagnosticTroubleCodes', 46 | '/': 'attributes', 47 | }; 48 | if (BATCH_PATH_TO_ATTRIBUTE[path]) { 49 | return BATCH_PATH_TO_ATTRIBUTE[path]; 50 | } 51 | 52 | // For everything else camelCase method from lodash works 53 | // Examples: '/battery/capacity', '/engine/oil', '/odometer', '/tesla/speedometer' 54 | // converts to 'batteryCapacity', 'engineOil', 'odometer', 'teslaSpeedometer' 55 | return _.camelCase(path); 56 | }; 57 | const buildBatchResponse = async(res) => { 58 | 59 | const {parsedBody: body, parsedHeaders: headers} = await util.handleRes(res); 60 | const batchResponse = {}; 61 | body.responses.forEach((response) => { 62 | const attributeName = getNameFromPath(response.path); 63 | if ([200, 204].includes(response.code)) { 64 | batchResponse[attributeName] = () => { 65 | return { 66 | ...(response.body || {}), 67 | meta: buildMeta({ 68 | ...headers, 69 | ...response.headers, 70 | }), 71 | }; 72 | }; 73 | } else { 74 | batchResponse[attributeName] = () => { 75 | util.handleError({body: response.body, headers: response.headers, res}); 76 | }; 77 | } 78 | }); 79 | return batchResponse; 80 | }; 81 | /** 82 | * Initializes a new Service object to make requests to the Smartcar API. 83 | * 84 | * @constructor 85 | * @param {Object} [options] 86 | * @param {String} [options.baseUrl] - Host/Base URL for the requests 87 | * @param {Object} [options.auth] - authorization options 88 | * @param {Object} [options.headers] - headers to add 89 | * @param {Object} [options.apiVersion] - API version to use for requests 90 | */ 91 | function SmartcarService(options = {}) { 92 | const defaultOptions = { 93 | json: true, 94 | headers: { 95 | 'User-Agent': util.USER_AGENT, 96 | 'Content-Type': 'application/json', 97 | }, 98 | timeout: config.timeout, 99 | }; 100 | this.baseUrl = options.baseUrl || config.api; 101 | this.defaultOptions = _.merge(defaultOptions, options); 102 | } 103 | 104 | function joinUrl(base, path) { 105 | return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`; 106 | } 107 | 108 | SmartcarService.prototype.request = async function( 109 | type, 110 | path, 111 | body = null, 112 | options = {}, 113 | ) { 114 | 115 | const requestOptions = _.merge( 116 | {}, 117 | this.defaultOptions, 118 | options, 119 | ); 120 | 121 | const method = type.toUpperCase(); 122 | const apiVersion = requestOptions.apiVersion || config.version; 123 | requestOptions.method = method; 124 | 125 | const {qs} = requestOptions; 126 | 127 | const params = new URLSearchParams(qs || {}); 128 | 129 | if (body) { 130 | const formEncoded = 'application/x-www-form-urlencoded'; 131 | if (requestOptions.headers['Content-Type'] === formEncoded) { 132 | requestOptions.body = new URLSearchParams(body); 133 | } else { 134 | requestOptions.body = JSON.stringify(body); 135 | } 136 | } 137 | 138 | // eslint-disable-next-line max-len 139 | const requestPath = joinUrl(this.baseUrl, path) + (params.toString() ? `?${params.toString()}` : ''); 140 | 141 | const newRequest = new Request(requestPath, requestOptions); 142 | 143 | try { 144 | const res = await fetch(newRequest, 145 | {signal: AbortSignal.timeout(requestOptions.timeout)}, 146 | ); 147 | 148 | const {parsedBody, parsedHeaders} = await util.handleRes(res); 149 | 150 | if (!res.ok) { 151 | util.handleError({body: parsedBody, headers: parsedHeaders, res}); 152 | } 153 | 154 | if (Array.isArray(parsedBody)) { 155 | return { 156 | data: parsedBody, 157 | meta: apiVersion === '3' ? parsedHeaders : buildMeta(parsedHeaders), 158 | }; 159 | } else { 160 | return { 161 | ...(parsedBody || {}), 162 | meta: apiVersion === '3' ? parsedHeaders : buildMeta(parsedHeaders), 163 | }; 164 | } 165 | } catch (error) { 166 | if (error.name === 'TimeoutError') { 167 | throw new SmartcarError( 168 | 408, 169 | 'Request timed out', 170 | {'content-type': 'application/json'}, 171 | ); 172 | } 173 | throw error; 174 | } 175 | }; 176 | 177 | SmartcarService.prototype.batchRequest = async function(paths) { 178 | const requests = paths.map((path) => ({path})); 179 | 180 | const requestPath = joinUrl(this.baseUrl, '/batch'); 181 | 182 | 183 | const requestOptions = { 184 | method: 'POST', 185 | ...this.defaultOptions, 186 | body: JSON.stringify({requests}), 187 | }; 188 | 189 | const newRequest = new Request(requestPath, requestOptions); 190 | 191 | try { 192 | const res = await fetch(newRequest, 193 | {signal: AbortSignal.timeout(requestOptions.timeout)}, 194 | ); 195 | 196 | if (!res.ok) { 197 | const {parsedBody, parsedHeaders} = await util.handleRes(res); 198 | util.handleError({body: parsedBody, headers: parsedHeaders, res}); 199 | } 200 | 201 | return buildBatchResponse(res); 202 | } catch (error) { 203 | if (error.name === 'TimeoutError') { 204 | throw new SmartcarError( 205 | 408, 206 | 'Request timed out', 207 | {'content-type': 'application/json'}, 208 | ); 209 | } 210 | throw error; 211 | 212 | } 213 | 214 | }; 215 | 216 | module.exports = SmartcarService; 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smartcar Node SDK [![Build Status][ci-image]][ci-url] [![npm version][npm-image]][npm-url] 2 | 3 | The official Smartcar Node SDK. 4 | 5 | ## Overview 6 | 7 | The [Smartcar API](https://smartcar.com/docs) lets you read vehicle data 8 | (location, odometer) and send commands to vehicles (lock, unlock) using HTTP requests. 9 | 10 | To make requests to a vehicle from a web or mobile application, the end user 11 | must connect their vehicle using 12 | [Smartcar Connect](https://smartcar.com/docs/api#smartcar-connect). 13 | This flow follows the OAuth spec and will return a `code` which can be used to 14 | obtain an access token from Smartcar. 15 | 16 | The Smartcar Node SDK provides methods to: 17 | 18 | 1. Generate the link to redirect to Connect. 19 | 2. Make a request to Smartcar with the `code` obtained from Connect to obtain an 20 | access and refresh token 21 | 3. Make requests to the Smartcar API to read vehicle data and send commands to 22 | vehicles using the access token obtained in step 2. 23 | 24 | Before integrating with Smartcar's SDK, you'll need to register an application 25 | in the [Smartcar Developer portal](https://developer.smartcar.com). If you do 26 | not have access to the dashboard, please 27 | [request access](https://smartcar.com/subscribe). 28 | 29 | On the Developer portal, you will need to set your application's `redirectURI` 30 | and `scope` permissions. 31 | 32 | ### Flow 33 | 34 | - Create a new `AuthClient` object with your `clientId`, and `clientSecret`. 35 | - Redirect the user to Smartcar Connect using `getAuthUrl` with required `scope` or with one 36 | of our frontend SDKs. 37 | - The user will login, and then accept or deny your `scope`'s permissions. 38 | - Handle the get request to `redirectUri`. 39 | - If the user accepted your permissions, `req.query.code` will contain an 40 | authorization code. 41 | - Use `exchangeCode` with this code to obtain an access object 42 | containing an access token (lasting 2 hours) and a refresh token 43 | (lasting 60 days). 44 | - Save this access object. 45 | - If the user denied your permissions, `req.query.error` will be set 46 | to `"access_denied"`. 47 | - If you passed a state parameter to `getAuthUrl`, `req.query.state` will 48 | contain the state value. 49 | - Get the user's vehicles with `getVehicles`. 50 | - Create a new `Vehicle` object using a `vehicleId` from the previous response, 51 | and the `access_token`. 52 | - Make requests to the Smartcar API. 53 | - Use `exchangeRefreshToken` on your saved `refreshToken` to retrieve a new token 54 | when your `accessToken` expires. 55 | 56 | ### Installation 57 | 58 | ```shell 59 | npm install smartcar --save 60 | ``` 61 | 62 | ### Example 63 | 64 | ```javascript 65 | 'use strict'; 66 | 67 | const smartcar = require('smartcar'); 68 | const express = require('express'); 69 | 70 | const app = express(); 71 | 72 | const port = 4000; 73 | 74 | const client = new smartcar.AuthClient({ 75 | clientId: '', // fallback to SMARTCAR_CLIENT_ID ENV variable 76 | clientSecret: '', // fallback to SMARTCAR_CLIENT_SECRET ENV variable 77 | mode: 'test', // launch Smartcar Connect in test mode 78 | }); 79 | 80 | // Redirect to Smartcar Connect 81 | app.get('/login', function(req, res) { 82 | const link = client.getAuthUrl(); 83 | 84 | // redirect to the link 85 | res.redirect(link); 86 | }); 87 | 88 | // Handle Smartcar callback with auth code 89 | app.get('/callback', async function(req, res, next) { 90 | let access; 91 | 92 | if (req.query.error) { 93 | // the user denied your requested permissions 94 | return next(new Error(req.query.error)); 95 | } 96 | 97 | // exchange auth code for access token 98 | const tokens = await client.exchangeCode(req.query.code); 99 | // get the user's vehicles 100 | const vehicles = await smartcar.getVehicles(tokens.accessToken); 101 | // instantiate first vehicle in vehicle list 102 | const vehicle = new smartcar.Vehicle( 103 | vehicles.vehicles[0], 104 | tokens.accessToken 105 | ); 106 | // get identifying information about a vehicle 107 | const attributes = await vehicle.attributes(); 108 | console.log(attributes); 109 | // { 110 | // "id": "36ab27d0-fd9d-4455-823a-ce30af709ffc", 111 | // "make": "TESLA", 112 | // "model": "Model S", 113 | // "year": 2014 114 | // "meta": { 115 | // "requestId": "ada7207c-3c0a-4027-a47f-6215ce6f7b93" 116 | // } 117 | // } 118 | 119 | // get signal all signal data for a vehicle 120 | const response = await vehicle.getSignals(); 121 | console.log(response.body); 122 | 123 | 124 | // or get a specific signal 125 | const odometerResponse = await vehicle.getSignal('odometer-traveleddistance'); 126 | console.log(odometerResponse.body); 127 | 128 | 129 | }); 130 | 131 | app.listen(port, () => console.log(`Listening on port ${port}`)); 132 | ``` 133 | 134 | ## SDK Reference 135 | 136 | For detailed documentation on parameters and available methods, please refer to 137 | the [SDK Reference](doc/readme.md). 138 | 139 | ## Contributing 140 | 141 | To contribute, please: 142 | 143 | 1. Open an issue for the feature (or bug) you would like to resolve. 144 | 2. Resolve the issue and add tests in your feature branch. 145 | 3. Open a PR from your feature branch into `develop` that tags the issue. 146 | 147 | To test: 148 | 149 | ```shell 150 | npm run test 151 | ``` 152 | 153 | Note: In order to run tests locally the following environment variables would have to be set : 154 | 155 | - `E2E_SMARTCAR_CLIENT_ID` - Client ID to be used. 156 | - `E2E_SMARTCAR_CLIENT_SECRET` - Client secret to be used. 157 | - `E2E_SMARTCAR_AMT` - AMT from dashboard for webhooks tests. 158 | - `E2E_SMARTCAR_WEBHOOK_ID` - Webhook ID use in the webhook tests success case. 159 | - `BROWSER` - Web browser for tests (`chrome` or `firefox`, default: `firefox`). 160 | 161 | Optionally you can set: 162 | - `SELENIUM_REMOTE_URL` - URL for standalone Selenium server (e.g. `http://localhost:4444/wd/hub`). 163 | - `SELENIUM_BROWSER` - Browser to be used by standalone Selenium server (`chrome` or `firefox`, default: `firefox`). 164 | 165 | Your application needs to have https://example.com/auth set as a valid redirect URI 166 | 167 | [ci-url]: https://travis-ci.com/smartcar/node-sdk 168 | [ci-image]: https://travis-ci.com/smartcar/node-sdk.svg?token=jMbuVtXPGeJMPdsn7RQ5&branch=master 169 | [npm-url]: https://badge.fury.io/js/smartcar 170 | [npm-image]: https://badge.fury.io/js/smartcar.svg 171 | 172 | ## Supported Node.js Versions 173 | 174 | Smartcar aims to support the SDK on all Node.js versions that have a status of "Maintenance" 175 | or "Active LTS" as defined in the 176 | [Node.js Release schedule](https://github.com/nodejs/Release#release-schedule). 177 | 178 | In accordance with the Semantic Versioning specification, the addition of support 179 | for new Node.js versions would result in a MINOR version bump and the removal of support 180 | for Node.js versions would result in a MAJOR version bump. 181 | -------------------------------------------------------------------------------- /doc/SmartcarService.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: SmartcarService 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Class: SmartcarService

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

SmartcarService(optionsopt)

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |

new SmartcarService(optionsopt)

45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |

Initializes a new Service object to make requests to the Smartcar API.

53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
Parameters:
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 101 | 102 | 103 | 112 | 113 | 114 | 115 | 116 | 244 | 245 | 246 | 247 | 248 |
NameTypeAttributesDescription
options 94 | 95 | 96 | Object 97 | 98 | 99 | 100 | 104 | 105 | <optional>
106 | 107 | 108 | 109 | 110 | 111 |
117 |
Properties
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 155 | 156 | 157 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 188 | 189 | 190 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 221 | 222 | 223 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 |
NameTypeAttributesDescription
baseUrl 148 | 149 | 150 | String 151 | 152 | 153 | 154 | 158 | 159 | <optional>
160 | 161 | 162 | 163 | 164 | 165 |

Host/Base URL for the requests

auth 181 | 182 | 183 | Object 184 | 185 | 186 | 187 | 191 | 192 | <optional>
193 | 194 | 195 | 196 | 197 | 198 |

authorization options

headers 214 | 215 | 216 | Object 217 | 218 | 219 | 220 | 224 | 225 | <optional>
226 | 227 | 228 | 229 | 230 | 231 |

headers to add

242 | 243 |
249 | 250 | 251 | 252 | 253 | 254 | 255 |
256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 |
Source:
283 |
286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |
294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 |
316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 |
337 | 338 |
339 | 340 | 341 | 342 | 343 |
344 | 345 | 348 | 349 |
350 | 351 |
352 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 353 |
354 | 355 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /test/unit/lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const test = require('ava'); 5 | const {env} = require('process'); 6 | 7 | const util = require('../../../lib/util'); 8 | const smartcar = require('../../../'); 9 | const config = require('../../../lib/config.json'); 10 | const SmartcarError = require('../../../lib/smartcar-error'); 11 | 12 | const API_URL = config.api + '/v' + config.version; 13 | 14 | test.afterEach((t) => { 15 | smartcar.setApiVersion('1.0'); 16 | t.true(config.version === '1.0'); 17 | }); 18 | 19 | test('formatAccess', function(t) { 20 | 21 | /* eslint-disable camelcase */ 22 | const response = { 23 | token_type: 'Bearer', 24 | expires_in: 7200, 25 | access_token: 'df12ca11-36a7-4196-9c33-b9fa1f18dd14', 26 | refresh_token: 'c569b7ed-6536-4d19-9790-61ba7a4389c6', 27 | }; 28 | /* eslint-enable camelcase */ 29 | const access = util.formatAccess(response); 30 | const expectedKeys = [ 31 | 'accessToken', 32 | 'refreshToken', 33 | 'expiration', 34 | 'refreshExpiration', 35 | ]; 36 | 37 | t.is(_.xor(Object.keys(access), expectedKeys).length, 0); 38 | 39 | t.is(access.accessToken, response.access_token); 40 | t.is(access.refreshToken, response.refresh_token); 41 | 42 | const expected = Date.now() + (7200 * 1000); 43 | const rExpected = Date.now() + (60 * 24 * 60 * 60 * 1000); 44 | 45 | t.true(_.isDate(access.expiration)); 46 | t.true(_.isDate(access.refreshExpiration)); 47 | 48 | const actual = access.expiration.getTime(); 49 | const rActual = access.refreshExpiration.getTime(); 50 | t.true(expected - 1000 <= actual && actual <= expected + 1000); 51 | t.true(rExpected - 1000 <= rActual && rActual <= rExpected + 1000); 52 | }); 53 | 54 | test('getOrThrowConfig - success', function(t) { 55 | env.PIZZA = 'PASTA'; 56 | t.is(util.getOrThrowConfig('PIZZA'), 'PASTA'); 57 | }); 58 | 59 | test('getOrThrowConfig - error', function(t) { 60 | const error = t.throws(() => (util.getOrThrowConfig('PASTA'))); 61 | t.is(error.message, 'PASTA not set or passed as arguments'); 62 | }); 63 | 64 | test('getUrl - no args', function(t) { 65 | const url = util.getUrl(); 66 | t.is(url, API_URL + '/vehicles'); 67 | }); 68 | 69 | test('getUrl - id', function(t) { 70 | const url = util.getUrl('VID'); 71 | t.is(url, API_URL + '/vehicles/VID'); 72 | }); 73 | 74 | test('getUrl - id & endpoint', function(t) { 75 | const url = util.getUrl('VID', 'odometer'); 76 | t.is(url, API_URL + '/vehicles/VID/odometer'); 77 | }); 78 | 79 | test('getUrl - version 2.0', function(t) { 80 | const url = util.getUrl('VID', 'odometer', '2.0'); 81 | t.is(url, 'https://api.smartcar.com/v2.0/vehicles/VID/odometer'); 82 | }); 83 | 84 | test('handleError - non-json', function(t) { 85 | const boxed = t.throws(() => util.handleError({ 86 | body: 'what', 87 | headers: {}, 88 | res: {status: 504}, 89 | })); 90 | 91 | t.true(boxed instanceof SmartcarError); 92 | t.is(boxed.statusCode, 504); 93 | t.is(boxed.message, 'what'); 94 | }); 95 | 96 | test('handleError - String/non-json body', function(t) { 97 | const boxed = t.throws(() => util.handleError({ 98 | body: 'what', 99 | headers: {'content-type': 'application/json'}, 100 | res: {status: 500}, 101 | })); 102 | 103 | t.true(boxed instanceof SmartcarError); 104 | t.is(boxed.statusCode, 500); 105 | t.is(boxed.message, 'SDK_ERROR:undefined - what'); 106 | t.is(boxed.type, 'SDK_ERROR'); 107 | }); 108 | 109 | test('handleError - SmartcarError V1', function(t) { 110 | const boxed = t.throws(() => util.handleError({ 111 | body: {error: 'monkeys_on_mars', 112 | message: 'yes, really', 113 | }, 114 | headers: {'content-type': 'application/json'}, 115 | res: {status: 600}, 116 | })); 117 | 118 | t.true(boxed instanceof SmartcarError); 119 | t.is(boxed.statusCode, 600); 120 | t.is(boxed.message, 'monkeys_on_mars:undefined - yes, really'); 121 | t.is(boxed.type, 'monkeys_on_mars'); 122 | }); 123 | 124 | test('handleError - when bit-flips because of moon position', function(t) { 125 | 126 | const boxed = t.throws(() => util.handleError({ 127 | body: { 128 | random: 'testing', 129 | }, 130 | headers: {'content-type': 'application/json'}, 131 | res: {status: 999}, 132 | })); 133 | 134 | t.true(boxed instanceof SmartcarError); 135 | t.is(boxed.statusCode, 999); 136 | t.is(boxed.message, 'SDK_ERROR:undefined - Unknown error'); 137 | t.is(boxed.type, 'SDK_ERROR'); 138 | }); 139 | 140 | 141 | test('handleError - SmartcarError V2 no resolution', function(t) { 142 | const boxed = t.throws(() => util.handleError({ 143 | body: { 144 | type: 'type', 145 | code: 'code', 146 | description: 'description', 147 | resolution: null, 148 | detail: null, 149 | requestId: '123', 150 | docURL: null, 151 | statusCode: 500, 152 | }, 153 | headers: {'content-type': 'application/json'}, 154 | res: {status: 500}, 155 | })); 156 | 157 | t.true(boxed instanceof SmartcarError); 158 | t.is(boxed.statusCode, 500); 159 | t.false('resolution' in boxed); 160 | t.false('detail' in boxed); 161 | t.false('docURL' in boxed); 162 | t.is(boxed.description, 'description'); 163 | t.is(boxed.type, 'type'); 164 | t.is(boxed.code, 'code'); 165 | t.is(boxed.requestId, '123'); 166 | t.is(boxed.message, 'type:code - description'); 167 | }); 168 | 169 | test('handleError - SmartcarError V2 resolution string', function(t) { 170 | 171 | const boxed = t.throws(() => util.handleError({ 172 | body: { 173 | type: 'type', 174 | code: 'code', 175 | description: 'description', 176 | resolution: {type: 'resolution'}, 177 | requestId: '123', 178 | statusCode: 500, 179 | }, 180 | headers: {'content-type': 'application/json'}, 181 | res: {status: 500}, 182 | })); 183 | 184 | t.true(boxed instanceof SmartcarError); 185 | t.is(boxed.statusCode, 500); 186 | t.is(boxed.resolution.type, 'resolution'); 187 | t.false('url' in boxed.resolution); 188 | t.false('detail' in boxed); 189 | t.false('docURL' in boxed); 190 | t.is(boxed.description, 'description'); 191 | t.is(boxed.type, 'type'); 192 | t.is(boxed.code, 'code'); 193 | t.is(boxed.requestId, '123'); 194 | t.is(boxed.message, 'type:code - description'); 195 | }); 196 | 197 | test('handleError - SmartcarError V2 with all attrbutes', function(t) { 198 | const boxed = t.throws(() => util.handleError({ 199 | body: { 200 | type: 'type', 201 | code: 'code', 202 | description: 'description', 203 | docURL: 'docURL', 204 | resolution: {pizza: 'resolution'}, 205 | requestId: '123', 206 | statusCode: 500, 207 | detail: ['pizza'], 208 | }, 209 | headers: {'content-type': 'application/json'}, 210 | res: {status: 500}, 211 | })); 212 | 213 | t.true(boxed instanceof SmartcarError); 214 | t.is(boxed.statusCode, 500); 215 | t.is(boxed.resolution.pizza, 'resolution'); 216 | t.is(boxed.docURL, 'docURL'); 217 | t.is(boxed.description, 'description'); 218 | t.is(boxed.type, 'type'); 219 | t.is(boxed.code, 'code'); 220 | t.is(boxed.requestId, '123'); 221 | t.is(boxed.message, 'type:code - description'); 222 | t.is(boxed.detail[0], 'pizza'); 223 | }); 224 | 225 | test('getManagementToken', function(t) { 226 | const res = util.getManagementToken('amt'); 227 | t.is(res, 'ZGVmYXVsdDphbXQ='); 228 | const res2 = util.getManagementToken('amt', 'default'); 229 | t.is(res2, 'ZGVmYXVsdDphbXQ='); 230 | }); 231 | -------------------------------------------------------------------------------- /doc/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('../fonts/OpenSans-Regular-webfont.eot'); 6 | src: 7 | local('Open Sans'), 8 | local('OpenSans'), 9 | url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), 11 | url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Open Sans Light'; 16 | font-weight: normal; 17 | font-style: normal; 18 | src: url('../fonts/OpenSans-Light-webfont.eot'); 19 | src: 20 | local('Open Sans Light'), 21 | local('OpenSans Light'), 22 | url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('../fonts/OpenSans-Light-webfont.woff') format('woff'), 24 | url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); 25 | } 26 | 27 | html 28 | { 29 | overflow: auto; 30 | background-color: #fff; 31 | font-size: 14px; 32 | } 33 | 34 | body 35 | { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.5; 38 | color: #4d4e53; 39 | background-color: white; 40 | } 41 | 42 | a, a:visited, a:active { 43 | color: #0095dd; 44 | text-decoration: none; 45 | } 46 | 47 | a:hover { 48 | text-decoration: underline; 49 | } 50 | 51 | header 52 | { 53 | display: block; 54 | padding: 0px 4px; 55 | } 56 | 57 | tt, code, kbd, samp { 58 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 59 | } 60 | 61 | .class-description { 62 | font-size: 130%; 63 | line-height: 140%; 64 | margin-bottom: 1em; 65 | margin-top: 1em; 66 | } 67 | 68 | .class-description:empty { 69 | margin: 0; 70 | } 71 | 72 | #main { 73 | float: left; 74 | width: 70%; 75 | } 76 | 77 | article dl { 78 | margin-bottom: 40px; 79 | } 80 | 81 | article img { 82 | max-width: 100%; 83 | } 84 | 85 | section 86 | { 87 | display: block; 88 | background-color: #fff; 89 | padding: 12px 24px; 90 | border-bottom: 1px solid #ccc; 91 | margin-right: 30px; 92 | } 93 | 94 | .variation { 95 | display: none; 96 | } 97 | 98 | .signature-attributes { 99 | font-size: 60%; 100 | color: #aaa; 101 | font-style: italic; 102 | font-weight: lighter; 103 | } 104 | 105 | nav 106 | { 107 | display: block; 108 | float: right; 109 | margin-top: 28px; 110 | width: 30%; 111 | box-sizing: border-box; 112 | border-left: 1px solid #ccc; 113 | padding-left: 16px; 114 | } 115 | 116 | nav ul { 117 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 118 | font-size: 100%; 119 | line-height: 17px; 120 | padding: 0; 121 | margin: 0; 122 | list-style-type: none; 123 | } 124 | 125 | nav ul a, nav ul a:visited, nav ul a:active { 126 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 127 | line-height: 18px; 128 | color: #4D4E53; 129 | } 130 | 131 | nav h3 { 132 | margin-top: 12px; 133 | } 134 | 135 | nav li { 136 | margin-top: 6px; 137 | } 138 | 139 | footer { 140 | display: block; 141 | padding: 6px; 142 | margin-top: 12px; 143 | font-style: italic; 144 | font-size: 90%; 145 | } 146 | 147 | h1, h2, h3, h4 { 148 | font-weight: 200; 149 | margin: 0; 150 | } 151 | 152 | h1 153 | { 154 | font-family: 'Open Sans Light', sans-serif; 155 | font-size: 48px; 156 | letter-spacing: -2px; 157 | margin: 12px 24px 20px; 158 | } 159 | 160 | h2, h3.subsection-title 161 | { 162 | font-size: 30px; 163 | font-weight: 700; 164 | letter-spacing: -1px; 165 | margin-bottom: 12px; 166 | } 167 | 168 | h3 169 | { 170 | font-size: 24px; 171 | letter-spacing: -0.5px; 172 | margin-bottom: 12px; 173 | } 174 | 175 | h4 176 | { 177 | font-size: 18px; 178 | letter-spacing: -0.33px; 179 | margin-bottom: 12px; 180 | color: #4d4e53; 181 | } 182 | 183 | h5, .container-overview .subsection-title 184 | { 185 | font-size: 120%; 186 | font-weight: bold; 187 | letter-spacing: -0.01em; 188 | margin: 8px 0 3px 0; 189 | } 190 | 191 | h6 192 | { 193 | font-size: 100%; 194 | letter-spacing: -0.01em; 195 | margin: 6px 0 3px 0; 196 | font-style: italic; 197 | } 198 | 199 | table 200 | { 201 | border-spacing: 0; 202 | border: 0; 203 | border-collapse: collapse; 204 | } 205 | 206 | td, th 207 | { 208 | border: 1px solid #ddd; 209 | margin: 0px; 210 | text-align: left; 211 | vertical-align: top; 212 | padding: 4px 6px; 213 | display: table-cell; 214 | } 215 | 216 | thead tr 217 | { 218 | background-color: #ddd; 219 | font-weight: bold; 220 | } 221 | 222 | th { border-right: 1px solid #aaa; } 223 | tr > th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .source 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .source code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /doc/lib_smartcar-service.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: lib/smartcar-service.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: lib/smartcar-service.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | 
 32 | const _ = require('lodash');
 33 | const SmartcarError = require('./smartcar-error');
 34 | const config = require('./config.json');
 35 | const util = require('./util');
 36 | const HEADER_TO_META_KEYS = {
 37 |   'sc-data-age': 'dataAge',
 38 |   'sc-unit-system': 'unitSystem',
 39 |   'sc-request-id': 'requestId',
 40 |   'sc-fetched-at': 'fetchedAt',
 41 | };
 42 | 
 43 | 
 44 | const buildMeta = (headers) => {
 45 |   const lowerCaseHeaders = _.mapKeys(
 46 |     headers,
 47 |     (_, key) => key.toLocaleLowerCase(),
 48 |   );
 49 |   const meta = {};
 50 |   Object.entries(HEADER_TO_META_KEYS).forEach(([headerName, key]) => {
 51 |     if (lowerCaseHeaders[headerName]) {
 52 |       meta[key] = lowerCaseHeaders[headerName];
 53 |     }
 54 |   });
 55 |   if (meta.dataAge) {
 56 |     meta.dataAge = new Date(meta.dataAge);
 57 |   }
 58 |   if (meta.fetchedAt) {
 59 |     meta.fetchedAt = new Date(meta.fetchedAt);
 60 |   }
 61 | 
 62 |   return meta;
 63 | };
 64 | 
 65 | const getNameFromPath = (path) => {
 66 |   // Using this constant for edge cases.
 67 |   // '/' should have a method name of 'attributes'
 68 |   // '/tires/pressure' should be tirePressure and NOT tiresPressure
 69 |   const BATCH_PATH_TO_ATTRIBUTE = {
 70 |     '/security': 'lockStatus',
 71 |     '/tires/pressure': 'tirePressure',
 72 |     '/diagnostics/system_status': 'diagnosticSystemStatus',
 73 |     '/diagnostics/dtcs': 'diagnosticTroubleCodes',
 74 |     '/': 'attributes',
 75 |   };
 76 |   if (BATCH_PATH_TO_ATTRIBUTE[path]) {
 77 |     return BATCH_PATH_TO_ATTRIBUTE[path];
 78 |   }
 79 | 
 80 |   // For everything else camelCase method from lodash works
 81 |   // Examples: '/battery/capacity', '/engine/oil', '/odometer', '/tesla/speedometer'
 82 |   // converts to 'batteryCapacity', 'engineOil', 'odometer', 'teslaSpeedometer'
 83 |   return _.camelCase(path);
 84 | };
 85 | const buildBatchResponse = async(res) => {
 86 | 
 87 |   const {parsedBody: body, parsedHeaders: headers} = await util.handleRes(res);
 88 |   const batchResponse = {};
 89 |   body.responses.forEach((response) => {
 90 |     const attributeName = getNameFromPath(response.path);
 91 |     if ([200, 204].includes(response.code)) {
 92 |       batchResponse[attributeName] = () => {
 93 |         return {
 94 |           ...(response.body || {}),
 95 |           meta: buildMeta({
 96 |             ...headers,
 97 |             ...response.headers,
 98 |           }),
 99 |         };
100 |       };
101 |     } else {
102 |       batchResponse[attributeName] = () => {
103 |         util.handleError({body: response.body, headers: response.headers, res});
104 |       };
105 |     }
106 |   });
107 |   return batchResponse;
108 | };
109 | /**
110 |  * Initializes a new Service object to make requests to the Smartcar API.
111 |  *
112 |  * @constructor
113 |  * @param {Object} [options]
114 |  * @param {String} [options.baseUrl] - Host/Base URL for the requests
115 |  * @param {Object} [options.auth] - authorization options
116 |  * @param {Object} [options.headers] - headers to add
117 |  */
118 | function SmartcarService(options = {}) {
119 |   const defaultOptions = {
120 |     json: true,
121 |     headers: {
122 |       'User-Agent': util.USER_AGENT,
123 |       'Content-Type': 'application/json',
124 |     },
125 |     timeout: config.timeout,
126 |   };
127 |   this.baseUrl = options.baseUrl || config.api;
128 |   this.defaultOptions = _.merge(defaultOptions, options);
129 | }
130 | 
131 | function joinUrl(base, path) {
132 |   return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
133 | }
134 | 
135 | SmartcarService.prototype.request = async function(
136 |   type,
137 |   path,
138 |   body = null,
139 |   options = {},
140 | ) {
141 | 
142 |   const requestOptions = _.merge(
143 |     {},
144 |     this.defaultOptions,
145 |     options,
146 |   );
147 | 
148 |   const method = type.toUpperCase();
149 |   requestOptions.method = method;
150 | 
151 |   const {qs} = requestOptions;
152 | 
153 |   const params = new URLSearchParams(qs || {});
154 | 
155 |   if (body) {
156 |     const formEncoded = 'application/x-www-form-urlencoded';
157 |     if (requestOptions.headers['Content-Type'] === formEncoded) {
158 |       requestOptions.body = new URLSearchParams(body);
159 |     } else {
160 |       requestOptions.body = JSON.stringify(body);
161 |     }
162 |   }
163 | 
164 |   // eslint-disable-next-line max-len
165 |   const requestPath = joinUrl(this.baseUrl, path) + (params.toString() ? `?${params.toString()}` : '');
166 | 
167 |   const newRequest = new Request(requestPath, requestOptions);
168 | 
169 |   try {
170 |     const res = await fetch(newRequest,
171 |       {signal: AbortSignal.timeout(requestOptions.timeout)},
172 |     );
173 | 
174 |     const {parsedBody, parsedHeaders} = await util.handleRes(res);
175 | 
176 |     if (!res.ok) {
177 |       util.handleError({body: parsedBody, headers: parsedHeaders, res});
178 |     }
179 | 
180 |     if (Array.isArray(parsedBody)) {
181 |       return {
182 |         data: parsedBody,
183 |         meta: buildMeta(parsedHeaders),
184 |       };
185 |     } else {
186 |       return {
187 |         ...(parsedBody || {}),
188 |         meta: buildMeta(parsedHeaders),
189 |       };
190 |     }
191 |   } catch (error) {
192 |     if (error.name === 'TimeoutError') {
193 |       throw new SmartcarError(
194 |         408,
195 |         'Request timed out',
196 |         {'content-type': 'application/json'},
197 |       );
198 |     }
199 |     throw error;
200 |   }
201 | };
202 | 
203 | SmartcarService.prototype.batchRequest = async function(paths) {
204 |   const requests = paths.map((path) => ({path}));
205 | 
206 |   const requestPath = joinUrl(this.baseUrl, '/batch');
207 | 
208 | 
209 |   const requestOptions = {
210 |     method: 'POST',
211 |     ...this.defaultOptions,
212 |     body: JSON.stringify({requests}),
213 |   };
214 | 
215 |   const newRequest = new Request(requestPath, requestOptions);
216 | 
217 |   try {
218 |     const res = await fetch(newRequest,
219 |       {signal: AbortSignal.timeout(requestOptions.timeout)},
220 |     );
221 | 
222 |     if (!res.ok) {
223 |       const {parsedBody, parsedHeaders} = await util.handleRes(res);
224 |       util.handleError({body: parsedBody, headers: parsedHeaders, res});
225 |     }
226 | 
227 |     return buildBatchResponse(res);
228 |   } catch (error) {
229 |     if (error.name === 'TimeoutError') {
230 |       throw new SmartcarError(
231 |         408,
232 |         'Request timed out',
233 |         {'content-type': 'application/json'},
234 |       );
235 |     }
236 |     throw error;
237 | 
238 |   }
239 | 
240 | };
241 | 
242 | module.exports = SmartcarService;
243 | 
244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 | 252 | 255 | 256 |
257 | 258 |
259 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 260 |
261 | 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |

Smartcar Node SDK Build Status npm version

47 |

The official Smartcar Node SDK.

48 |

Overview

49 |

The Smartcar API lets you read vehicle data 50 | (location, odometer) and send commands to vehicles (lock, unlock) using HTTP requests.

51 |

To make requests to a vehicle from a web or mobile application, the end user 52 | must connect their vehicle using 53 | Smartcar Connect. 54 | This flow follows the OAuth spec and will return a code which can be used to 55 | obtain an access token from Smartcar.

56 |

The Smartcar Node SDK provides methods to:

57 |
    58 |
  1. Generate the link to redirect to Connect.
  2. 59 |
  3. Make a request to Smartcar with the code obtained from Connect to obtain an 60 | access and refresh token
  4. 61 |
  5. Make requests to the Smartcar API to read vehicle data and send commands to 62 | vehicles using the access token obtained in step 2.
  6. 63 |
64 |

Before integrating with Smartcar's SDK, you'll need to register an application 65 | in the Smartcar Developer portal. If you do 66 | not have access to the dashboard, please 67 | request access.

68 |

Flow

69 |
    70 |
  • Create a new AuthClient object with your clientId, clientSecret, 71 | redirectUri.
  • 72 |
  • Redirect the user to Smartcar Connect using getAuthUrl with required scope or with one 73 | of our frontend SDKs.
  • 74 |
  • The user will login, and then accept or deny your scope's permissions.
  • 75 |
  • Handle the get request to redirectUri. 76 |
      77 |
    • If the user accepted your permissions, req.query.code will contain an 78 | authorization code. 79 |
        80 |
      • Use exchangeCode with this code to obtain an access object 81 | containing an access token (lasting 2 hours) and a refresh token 82 | (lasting 60 days). 83 |
          84 |
        • Save this access object.
        • 85 |
        86 |
      • 87 |
      • If the user denied your permissions, req.query.error will be set 88 | to "access_denied".
      • 89 |
      • If you passed a state parameter to getAuthUrl, req.query.state will 90 | contain the state value.
      • 91 |
      92 |
    • 93 |
    94 |
  • 95 |
  • Get the user's vehicles with getVehicles.
  • 96 |
  • Create a new Vehicle object using a vehicleId from the previous response, 97 | and the access_token.
  • 98 |
  • Make requests to the Smartcar API.
  • 99 |
  • Use exchangeRefreshToken on your saved refreshToken to retrieve a new token 100 | when your accessToken expires.
  • 101 |
102 |

Installation

103 |
npm install smartcar --save
104 | 
105 |

Example

106 |
'use strict';
107 | 
108 | const smartcar = require('smartcar');
109 | const express = require('express');
110 | 
111 | const app = express();
112 | 
113 | const port = 4000;
114 | 
115 | const client = new smartcar.AuthClient({
116 |   clientId: '<Smartcar Client Id>', // fallback to SMARTCAR_CLIENT_ID ENV variable
117 |   clientSecret: '<Smartcar Client Secret>', // fallback to SMARTCAR_CLIENT_SECRET ENV variable
118 |   redirectUri: '<Your callback URI>', // fallback to SMARTCAR_REDIRECT_URI ENV variable
119 |   mode: 'test', // launch Smartcar Connect in test mode
120 | });
121 | 
122 | // Redirect to Smartcar Connect
123 | app.get('/login', function(req, res) {
124 |   const link = client.getAuthUrl(['read_vehicle_info']);
125 | 
126 |   // redirect to the link
127 |   res.redirect(link);
128 | });
129 | 
130 | // Handle Smartcar callback with auth code
131 | app.get('/callback', async function(req, res, next) {
132 |   let access;
133 | 
134 |   if (req.query.error) {
135 |     // the user denied your requested permissions
136 |     return next(new Error(req.query.error));
137 |   }
138 | 
139 |   // exchange auth code for access token
140 |   const tokens = await client.exchangeCode(req.query.code);
141 |   // get the user's vehicles
142 |   const vehicles = await smartcar.getVehicles(tokens.accessToken);
143 |   // instantiate first vehicle in vehicle list
144 |   const vehicle = new smartcar.Vehicle(
145 |     vehicles.vehicles[0],
146 |     tokens.accessToken
147 |   );
148 |   // get identifying information about a vehicle
149 |   const attributes = await vehicle.attributes();
150 |   console.log(attributes);
151 |   // {
152 |   //   "id": "36ab27d0-fd9d-4455-823a-ce30af709ffc",
153 |   //   "make": "TESLA",
154 |   //   "model": "Model S",
155 |   //   "year": 2014
156 |   //   "meta": {
157 |   //     "requestId": "ada7207c-3c0a-4027-a47f-6215ce6f7b93"
158 |   //   }
159 |   // }
160 | });
161 | 
162 | app.listen(port, () => console.log(`Listening on port ${port}`));
163 | 
164 |

SDK Reference

165 |

For detailed documentation on parameters and available methods, please refer to 166 | the SDK Reference.

167 |

Contributing

168 |

To contribute, please:

169 |
    170 |
  1. Open an issue for the feature (or bug) you would like to resolve.
  2. 171 |
  3. Resolve the issue and add tests in your feature branch.
  4. 172 |
  5. Open a PR from your feature branch into develop that tags the issue.
  6. 173 |
174 |

To test:

175 |
npm run test
176 | 
177 |

Note: In order to run tests locally the following environment variables would have to be set :

178 |
    179 |
  • E2E_SMARTCAR_CLIENT_ID - Client ID to be used.
  • 180 |
  • E2E_SMARTCAR_CLIENT_SECRET - Client secret to be used.
  • 181 |
  • E2E_SMARTCAR_AMT - AMT from dashboard for webhooks tests.
  • 182 |
  • E2E_SMARTCAR_WEBHOOK_ID - Webhook ID use in the webhook tests success case.
  • 183 |
  • BROWSER - Web browser for tests (chrome or firefox, default: firefox).
  • 184 |
185 |

Your application needs to have https://example.com/auth set as a valid redirect URI

186 |

Supported Node.js Versions

187 |

Smartcar aims to support the SDK on all Node.js versions that have a status of "Maintenance" or "Active LTS" as defined in the Node.js Release schedule.

188 |

In accordance with the Semantic Versioning specification, the addition of support for new Node.js versions would result in a MINOR version bump and the removal of support for Node.js versions would result in a MAJOR version bump.

189 |
190 | 191 | 192 | 193 | 194 | 195 | 196 |
197 | 198 | 201 | 202 |
203 | 204 |
205 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 206 |
207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /test/unit/lib/vehicle-coverage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nock = require('nock'); 4 | const test = require('ava'); 5 | 6 | const Vehicle = require('../../../lib/vehicle'); 7 | const {USER_AGENT} = require('../../../lib/util'); 8 | 9 | const VID = 'ada7207c-3c0a-4027-a47f-6215ce6f7b93'; 10 | const TOKEN = '9ad942c6-32b8-4af2-ada6-5e8ecdbad9c2'; 11 | 12 | const vehicle = new Vehicle(VID, TOKEN); 13 | 14 | const nocks = { 15 | base(version = vehicle.version, vid = VID, token = TOKEN) { 16 | return nock(`https://api.smartcar.com/v${version}/vehicles/${vid}`) 17 | .matchHeader('User-Agent', USER_AGENT) 18 | .matchHeader('Authorization', `Bearer ${token}`); 19 | }, 20 | }; 21 | 22 | test.afterEach(function(t) { 23 | if (t.context.n) { 24 | t.true(t.context.n.isDone()); 25 | } 26 | }); 27 | 28 | // Test for diagnosticSystemStatus method 29 | test('request - diagnostic system status', async function(t) { 30 | const responseBody = { 31 | systems: [ 32 | { 33 | systemId: 'POWERTRAIN', 34 | status: 'OK', 35 | }, 36 | { 37 | systemId: 'BATTERY', 38 | status: 'WARNING', 39 | }, 40 | ], 41 | }; 42 | 43 | t.context.n = nocks 44 | .base() 45 | .get('/diagnostics/system_status') 46 | .reply(200, responseBody, { 47 | 'sc-request-id': 'requestId', 48 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 49 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 50 | }); 51 | 52 | const response = await vehicle.diagnosticSystemStatus(); 53 | 54 | t.deepEqual(response.systems, responseBody.systems); 55 | t.is(response.meta.requestId, 'requestId'); 56 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 57 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 58 | }); 59 | 60 | // Test for diagnosticTroubleCodes method 61 | test('request - diagnostic trouble codes', async function(t) { 62 | const responseBody = { 63 | activeCodes: [ 64 | { 65 | code: 'P0100', 66 | description: 'Mass or Volume Air Flow Circuit Malfunction', 67 | timestamp: '2022-01-01T00:00:00.000Z', 68 | }, 69 | ], 70 | }; 71 | 72 | t.context.n = nocks 73 | .base() 74 | .get('/diagnostics/dtcs') 75 | .reply(200, responseBody, { 76 | 'sc-request-id': 'requestId', 77 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 78 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 79 | }); 80 | 81 | const response = await vehicle.diagnosticTroubleCodes(); 82 | 83 | t.deepEqual(response.activeCodes, responseBody.activeCodes); 84 | t.is(response.meta.requestId, 'requestId'); 85 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 86 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 87 | }); 88 | 89 | // Test for serviceHistory method 90 | test('request - service history', async function(t) { 91 | const startDate = '2022-01-01'; 92 | const endDate = '2022-12-31'; 93 | const responseBody = { 94 | serviceHistory: [ 95 | { 96 | serviceId: 'service-123', 97 | serviceTasks: [ 98 | { 99 | taskId: 'task-456', 100 | taskDescription: 'Oil change', 101 | }, 102 | ], 103 | serviceDate: '2022-06-15', 104 | odometerDistance: 12500.5, 105 | serviceDetails: [ 106 | { 107 | type: 'OIL_CHANGE', 108 | value: null, 109 | }, 110 | ], 111 | serviceCost: { 112 | totalCost: 49.99, 113 | currency: 'USD', 114 | }, 115 | }, 116 | ], 117 | }; 118 | 119 | t.context.n = nocks 120 | .base() 121 | .get('/service/history') 122 | .query({ 123 | // eslint-disable-next-line camelcase 124 | start_date: startDate, 125 | // eslint-disable-next-line camelcase 126 | end_date: endDate, 127 | }) 128 | .reply(200, responseBody, { 129 | 'sc-request-id': 'requestId', 130 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 131 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 132 | }); 133 | 134 | const response = await vehicle.serviceHistory(startDate, endDate); 135 | 136 | t.deepEqual(response.serviceHistory, responseBody.serviceHistory); 137 | t.is(response.meta.requestId, 'requestId'); 138 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 139 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 140 | }); 141 | 142 | // Test for sendDestination method 143 | test('request - send destination', async function(t) { 144 | const latitude = 37.7749; 145 | const longitude = -122.4194; 146 | const responseBody = { 147 | status: 'success', 148 | message: 'Successfully sent request to vehicle', 149 | }; 150 | 151 | t.context.n = nocks 152 | .base() 153 | .post('/navigation/destination', { 154 | latitude, 155 | longitude, 156 | }) 157 | .reply(200, responseBody, { 158 | 'sc-request-id': 'requestId', 159 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 160 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 161 | }); 162 | 163 | const response = await vehicle.sendDestination(latitude, longitude); 164 | 165 | t.is(response.status, responseBody.status); 166 | t.is(response.message, responseBody.message); 167 | t.is(response.meta.requestId, 'requestId'); 168 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 169 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 170 | }); 171 | 172 | // Test for sendDestination method with invalid latitude 173 | test('request - send destination with invalid latitude', async function(t) { 174 | const error = await t.throwsAsync( 175 | async() => { 176 | await vehicle.sendDestination(100, -122.4194); 177 | }, 178 | {instanceOf: Error}, 179 | ); 180 | 181 | t.is( 182 | error.message, 183 | 'Invalid latitude value. It must be between -90 and 90.', 184 | ); 185 | }); 186 | 187 | // Test for sendDestination method with invalid longitude 188 | test('request - send destination with invalid longitude', async function(t) { 189 | const error = await t.throwsAsync( 190 | async() => { 191 | await vehicle.sendDestination(37.7749, 200); 192 | }, 193 | {instanceOf: Error}, 194 | ); 195 | 196 | t.is( 197 | error.message, 198 | 'Invalid longitude value. It must be between -180 and 180.', 199 | ); 200 | }); 201 | 202 | // Test the request with a body 203 | test('request - with body', async function(t) { 204 | const requestBody = { 205 | someKey: 'someValue', 206 | }; 207 | 208 | const responseBody = { 209 | status: 'success', 210 | }; 211 | 212 | t.context.n = nocks 213 | .base() 214 | .post('/custom/endpoint', {body: requestBody}) 215 | .reply(200, responseBody, { 216 | 'sc-request-id': 'requestId', 217 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 218 | }); 219 | 220 | const response = await vehicle.request('post', 'custom/endpoint', { 221 | body: requestBody, 222 | }); 223 | 224 | t.is(response.body.status, 'success'); 225 | t.is(response.meta.requestId, 'requestId'); 226 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 227 | }); 228 | 229 | // Test the request without a body 230 | test('request - without body', async function(t) { 231 | const responseBody = { 232 | status: 'success', 233 | }; 234 | 235 | t.context.n = nocks 236 | .base() 237 | .get('/custom/endpoint') 238 | .reply(200, responseBody, { 239 | 'sc-request-id': 'requestId', 240 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 241 | }); 242 | 243 | const response = await vehicle.request('get', 'custom/endpoint', null); 244 | 245 | t.is(response.body.status, 'success'); 246 | t.is(response.meta.requestId, 'requestId'); 247 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 248 | }); 249 | 250 | // Test the request with null body 251 | test('request - with null body', async function(t) { 252 | 253 | await vehicle.request('get', 'custom/endpoint', { 254 | body: null, 255 | }).catch((err) => { 256 | // Handle the error if needed 257 | t.is(err.message, 'Request with GET/HEAD method cannot have body.'); 258 | }); 259 | 260 | }); 261 | 262 | // Test the request with false body 263 | test('request - with false body', async function(t) { 264 | 265 | await vehicle.request('get', 'custom/endpoint', { 266 | body: false, 267 | }).catch((err) => { 268 | // Handle the error if needed 269 | t.is(err.message, 'Request with GET/HEAD method cannot have body.'); 270 | }); 271 | 272 | }); 273 | 274 | 275 | 276 | // Test the request with string body 277 | test('request - with string body', async function(t) { 278 | const responseBody = { 279 | status: 'success', 280 | }; 281 | 282 | t.context.n = nocks 283 | .base() 284 | .post('/custom/endpoint', {body: 'test-string'}) 285 | .reply(200, responseBody, { 286 | 'sc-request-id': 'requestId', 287 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 288 | }); 289 | 290 | const response = await vehicle.request('post', 'custom/endpoint', { 291 | body: 'test-string', 292 | }); 293 | 294 | t.is(response.body.status, 'success'); 295 | t.is(response.meta.requestId, 'requestId'); 296 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 297 | }); 298 | 299 | -------------------------------------------------------------------------------- /lib/auth-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {emitWarning} = require('process'); 4 | const qs = require('querystring'); 5 | const SmartcarService = require('./smartcar-service'); 6 | const util = require('./util'); 7 | const config = require('./config.json'); 8 | 9 | /** @exports AuthClient*/ 10 | 11 | /** 12 | * @type {Object} 13 | * @typedef Access 14 | * @property {Date} expiration - Date object which represents when the access 15 | * token expires. 16 | * @property {String} accessToken - A token to be used for requests to the 17 | * Smartcar API 18 | * @property {String} refreshToken - A token which is used to renew access when 19 | * the current access token expires, expires in 60 days 20 | * @property {Date} refreshExpiration - Date object which represents when the 21 | * refresh token expires. 22 | * @example 23 | * { 24 | * expiration: new Date('2017-05-26T01:21:27.070Z'), 25 | * accessToken: '88704225-9f6c-4919-93e7-e0cec71317ce', 26 | * refreshToken: '60a9e801-6d26-4d88-926e-5c7f9fc13486', 27 | * refreshExpiration: new Date('2017-05-26T01:21:27.070Z'), 28 | * } 29 | */ 30 | 31 | /** 32 | * Create a Smartcar OAuth client for your application. 33 | * 34 | * @constructor 35 | * @param {Object} options 36 | * @param {String} options.clientId - Application client id obtained from 37 | * [Smartcar Developer Portal](https://developer.smartcar.com). If you do not 38 | * have access to the dashboard, please 39 | * [request access](https://smartcar.com/subscribe). 40 | * @param {String} options.clientSecret - The application's client secret. 41 | * @param {String} [options.redirectUri] - Redirect URI registered in the 42 | * [application settings](https://developer.smartcar.com/apps). The given URL 43 | * must exactly match one of the registered URLs. This parameter is optional 44 | * and should normally be set within the Smartcar Dashboard. 45 | * @param {Boolean} [options.testMode=false] - Deprecated, please use `mode` instead. 46 | * Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/). 47 | * @param {String} [options.mode='live'] - Determine what mode Smartcar Connect should be 48 | * launched in. Should be one of test, live or simulated. 49 | */ 50 | function AuthClient(options = {}) { 51 | this.clientId = 52 | options.clientId || util.getOrThrowConfig('SMARTCAR_CLIENT_ID'); 53 | this.clientSecret = 54 | options.clientSecret || util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET'); 55 | this.redirectUri = 56 | options.redirectUri; 57 | 58 | this.mode = 'live'; 59 | if (options.hasOwnProperty('testMode')) { 60 | emitWarning(// eslint-disable-next-line max-len 61 | 'The "testMode" parameter is deprecated, please use the "mode" parameter instead.', 62 | ); 63 | this.mode = options.testMode === true ? 'test' : 'live'; 64 | } else if (options.hasOwnProperty('mode')) { 65 | this.mode = options.mode; 66 | } 67 | if (!['test', 'live', 'simulated'].includes(this.mode)) { 68 | throw new Error(// eslint-disable-next-line max-len 69 | 'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'', 70 | ); 71 | } 72 | this.authUrl = util.getConfig('SMARTCAR_AUTH_ORIGIN') || config.auth; 73 | // eslint-disable-next-line max-len 74 | const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); 75 | 76 | this.service = new SmartcarService({ 77 | baseUrl: this.authUrl, 78 | headers: { 79 | 'Content-Type': 'application/x-www-form-urlencoded', 80 | Authorization: `Basic ${credentials}`, 81 | }, 82 | }); 83 | } 84 | 85 | /** 86 | * Generate the Smartcar Connect URL. 87 | * 88 | * By default users are not shown the permission dialog if they have already 89 | * approved the set of scopes for this application. The application can elect 90 | * to always display the permissions dialog to the user by setting 91 | * approval_prompt to `force`. 92 | * @param {String[]} [scope] - List of permissions your application 93 | * requires. The valid permission names are found in the 94 | * [API Reference](https://smartcar.com/docs/guides/scope/). This is an optional parameter 95 | * that should normally be set up within the Smartcar Dashboard. 96 | * @param {Object} [options] 97 | * @param {Boolean} [options.forcePrompt] - Setting `forcePrompt` to 98 | * `true` will show the permissions approval screen on every authentication 99 | * attempt, even if the user has previously consented to the exact scope of 100 | * permissions. 101 | * @param {Boolean|Object} [options.singleSelect] - An optional value that sets the 102 | * behavior of the grant dialog displayed to the user. Object can contain two keys : 103 | * - enabled - Boolean value, if set to `true`, `single_select` limits the user to 104 | * selecting only one vehicle. 105 | * - vin - String vin, if set, Smartcar will only authorize the vehicle with the specified VIN. 106 | * See the [Single Select guide](https://smartcar.com/docs/guides/single-select/) for more information. 107 | * @param {String} [options.state] - OAuth state parameter passed to the 108 | * redirect uri. This parameter may be used for identifying the user who 109 | * initiated the request. 110 | * @param {Object} [options.makeBypass] - An optional parameter that allows 111 | * users to bypass the car brand selection screen. 112 | * For a complete list of supported makes, please see our 113 | * [API Reference](https://smartcar.com/docs/api#authorization) documentation. 114 | * @param {Object} [options.flags] - Object of flags where key is the name of the flag 115 | * value is string or boolean value. 116 | * @param {String} [options.user] - An optional unique identifier for a vehicle owner. 117 | * This identifier is used to aggregate analytics across Connect sessions for each vehicle owner. 118 | * 119 | * @example 120 | * // Called with `scope` parameter. 121 | * authClient.getAuthUrl(['read_odometer', 'read_vehicle_info'], { 122 | * user: '61a3e3d2-5198-47ba-aabd-4623ce4a4042' 123 | * }); 124 | * 125 | * @example 126 | * // Called with only `options` parameter. 127 | * authClient.getAuthUrl({ 128 | * user: '61a3e3d2-5198-47ba-aabd-4623ce4a4042' 129 | * }); 130 | * 131 | * @return {String} Smartcar Connect URL to direct user to. 132 | * @example 133 | * https://connect.smartcar.com/oauth/authorize? 134 | * response_type=code 135 | * &client_id=8229df9f-91a0-4ff0-a1ae-a1f38ee24d07 136 | * &scope=read_odometer read_vehicle_info 137 | * &redirect_uri=https://example.com/home 138 | * &state=0facda3319 139 | * &make=TESLA 140 | * &single_select=true 141 | * &single_select_vin=5YJSA1E14FF101307 142 | * &flags=country:DE color:00819D 143 | * &user=61a3e3d2-5198-47ba-aabd-4623ce4a4042 144 | */ 145 | AuthClient.prototype.getAuthUrl = function(scopeOrOptions, options = {}) { 146 | let scope; 147 | if (Array.isArray(scopeOrOptions)) { 148 | scope = scopeOrOptions; 149 | options = options || {}; 150 | } else if (typeof scopeOrOptions === 'object' && scopeOrOptions !== null) { 151 | options = scopeOrOptions; 152 | scopeOrOptions = undefined; 153 | } 154 | /* eslint-disable camelcase */ 155 | const parameters = { 156 | response_type: 'code', 157 | client_id: this.clientId, 158 | approval_prompt: options.forcePrompt === true ? 'force' : 'auto', 159 | }; 160 | /* eslint-enable camelcase */ 161 | 162 | if (this.redirectUri) { 163 | // eslint-disable-next-line camelcase 164 | parameters.redirect_uri = this.redirectUri; 165 | } 166 | 167 | if (scope) { 168 | parameters.scope = scope.join(' '); 169 | } 170 | 171 | if (options.state) { 172 | parameters.state = options.state; 173 | } 174 | 175 | if (options.singleSelect) { 176 | /* eslint-disable camelcase */ 177 | if (options.singleSelect.vin) { 178 | parameters.single_select = true; 179 | parameters.single_select_vin = options.singleSelect.vin; 180 | } else if ([true, false].includes(options.singleSelect.enabled)) { 181 | parameters.single_select = options.singleSelect.enabled; 182 | } 183 | /* eslint-enable camelcase */ 184 | } 185 | 186 | if (options.makeBypass) { 187 | parameters.make = options.makeBypass; 188 | } 189 | 190 | if (options.flags) { 191 | parameters.flags = util.getFlagsString(options.flags); 192 | } 193 | 194 | if (options.user) { 195 | parameters.user = options.user; 196 | } 197 | 198 | parameters.mode = this.mode; 199 | 200 | const query = qs.stringify(parameters); 201 | 202 | 203 | return `${util.getConfig('SMARTCAR_API_ORIGIN') || config.connect 204 | }/oauth/authorize?${query}`; 205 | }; 206 | 207 | /** 208 | * Exchange an authorization code for an access object. 209 | * @async 210 | * @param {String} code - Authorization code to exchange for a Smartcar 211 | * access token and refresh token. 212 | * @param {Object} [options.flags] - Object of flags where key is the name of the flag 213 | * value is string or boolean value. 214 | * @return {Promise} New set of Access and Refresh tokens. 215 | * @throws {SmartcarError} - an instance of SmartcarError. 216 | * See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors) 217 | * for all possible errors. 218 | */ 219 | AuthClient.prototype.exchangeCode = async function(code, options = {}) { 220 | const qs = {}; 221 | if (options.flags) { 222 | qs.flags = util.getFlagsString(options.flags); 223 | } 224 | 225 | /* eslint-disable camelcase */ 226 | const form = { 227 | code, 228 | grant_type: 'authorization_code', 229 | redirect_uri: this.redirectUri, 230 | }; 231 | /* eslint-enable camelcase */ 232 | 233 | const response = await this.service.request( 234 | 'post', 235 | '/oauth/token', 236 | form, 237 | {qs}, 238 | ); 239 | return util.formatAccess(response); 240 | }; 241 | 242 | /** 243 | * Exchange a refresh token for a new access object. 244 | * @async 245 | * @param {String} token - Refresh token to exchange for a new set of Access and 246 | * Refresh tokens. 247 | * @param {Object} [options.flags] - Object of flags where key is the name of the flag 248 | * value is string or boolean value. 249 | * @return {Promise} New set of Access and Refresh tokens. 250 | * @throws {SmartcarError} - an instance of SmartcarError. 251 | * See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors) 252 | * for all possible errors. 253 | */ 254 | AuthClient.prototype.exchangeRefreshToken = async function( 255 | token, 256 | options = {}, 257 | ) { 258 | const qs = {}; 259 | if (options.flags) { 260 | qs.flags = util.getFlagsString(options.flags); 261 | } 262 | 263 | /* eslint-disable camelcase */ 264 | const form = { 265 | grant_type: 'refresh_token', 266 | refresh_token: token, 267 | }; 268 | /* eslint-enable camelcase */ 269 | 270 | const response = await this.service.request( 271 | 'post', 272 | '/oauth/token', 273 | form, 274 | {qs}, 275 | ); 276 | return util.formatAccess(response); 277 | }; 278 | 279 | module.exports = AuthClient; 280 | -------------------------------------------------------------------------------- /doc/lib_auth-client.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: lib/auth-client.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: lib/auth-client.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | const {emitWarning} = require('process');
 32 | const qs = require('querystring');
 33 | const SmartcarService = require('./smartcar-service');
 34 | const util = require('./util');
 35 | const config = require('./config.json');
 36 | 
 37 | /** @exports AuthClient*/
 38 | 
 39 | /**
 40 |  * @type {Object}
 41 |  * @typedef Access
 42 |  * @property {Date} expiration - Date object which represents when the access
 43 |  * token expires.
 44 |  * @property {String} accessToken - A token to be used for requests to the
 45 |  * Smartcar API
 46 |  * @property {String} refreshToken - A token which is used to renew access when
 47 |  * the current access token expires, expires in 60 days
 48 |  * @property {Date} refreshExpiration - Date object which represents when the
 49 |  * refresh token expires.
 50 |  * @example
 51 |  * {
 52 |  *   expiration: new Date('2017-05-26T01:21:27.070Z'),
 53 |  *   accessToken: '88704225-9f6c-4919-93e7-e0cec71317ce',
 54 |  *   refreshToken: '60a9e801-6d26-4d88-926e-5c7f9fc13486',
 55 |  *   refreshExpiration: new Date('2017-05-26T01:21:27.070Z'),
 56 |  * }
 57 |  */
 58 | 
 59 | /**
 60 |  * Create a Smartcar OAuth client for your application.
 61 |  *
 62 |  * @constructor
 63 |  * @param {Object} options
 64 |  * @param {String} options.clientId - Application client id obtained from
 65 |  * [Smartcar Developer Portal](https://developer.smartcar.com). If you do not
 66 |  * have access to the dashboard, please
 67 |  * [request access](https://smartcar.com/subscribe).
 68 |  * @param {String} options.clientSecret - The application's client secret.
 69 |  * @param {String} options.redirectUri - Redirect URI registered in the
 70 |  * [application settings](https://developer.smartcar.com/apps). The given URL
 71 |  * must exactly match one of the registered URLs.
 72 |  * @param {Boolean} [options.testMode=false] - Deprecated, please use `mode` instead.
 73 |  * Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
 74 |  * @param {String} [options.mode='live'] - Determine what mode Smartcar Connect should be
 75 |  * launched in. Should be one of test, live or simulated.
 76 |  */
 77 | function AuthClient(options = {}) {
 78 |   this.clientId =
 79 |     options.clientId || util.getOrThrowConfig('SMARTCAR_CLIENT_ID');
 80 |   this.clientSecret =
 81 |     options.clientSecret || util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET');
 82 |   this.redirectUri =
 83 |     options.redirectUri || util.getOrThrowConfig('SMARTCAR_REDIRECT_URI');
 84 | 
 85 |   this.mode = 'live';
 86 |   if (options.hasOwnProperty('testMode')) {
 87 |     emitWarning(// eslint-disable-next-line max-len
 88 |       'The "testMode" parameter is deprecated, please use the "mode" parameter instead.',
 89 |     );
 90 |     this.mode = options.testMode === true ? 'test' : 'live';
 91 |   } else if (options.hasOwnProperty('mode')) {
 92 |     this.mode = options.mode;
 93 |   }
 94 |   if (!['test', 'live', 'simulated'].includes(this.mode)) {
 95 |     throw new Error(// eslint-disable-next-line max-len
 96 |       'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'',
 97 |     );
 98 |   }
 99 |   this.authUrl = util.getConfig('SMARTCAR_AUTH_ORIGIN') || config.auth;
100 |   // eslint-disable-next-line max-len
101 |   const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
102 | 
103 |   this.service = new SmartcarService({
104 |     baseUrl: this.authUrl,
105 |     headers: {
106 |       'Content-Type': 'application/x-www-form-urlencoded',
107 |       Authorization: `Basic ${credentials}`,
108 |     },
109 |   });
110 | }
111 | 
112 | /**
113 |  * Generate the Smartcar Connect URL.
114 |  *
115 |  * By default users are not shown the permission dialog if they have already
116 |  * approved the set of scopes for this application. The application can elect
117 |  * to always display the permissions dialog to the user by setting
118 |  * approval_prompt to `force`.
119 |  * @param {String[]} [scope] - List of permissions your application
120 |  * requires. The valid permission names are found in the [API Reference](https://smartcar.com/docs/guides/scope/)
121 |  * @param {Object} [options]
122 |  * @param {Boolean} [options.forcePrompt] - Setting `forcePrompt` to
123 |  * `true` will show the permissions approval screen on every authentication
124 |  * attempt, even if the user has previously consented to the exact scope of
125 |  * permissions.
126 |  * @param {Boolean|Object} [options.singleSelect] - An optional value that sets the
127 |  * behavior of the grant dialog displayed to the user. Object can contain two keys :
128 |  *  - enabled - Boolean value, if set to `true`, `single_select` limits the user to
129 |  *    selecting only one vehicle.
130 |  *  - vin - String vin, if set, Smartcar will only authorize the vehicle with the specified VIN.
131 |  * See the [Single Select guide](https://smartcar.com/docs/guides/single-select/) for more information.
132 |  * @param {String} [options.state] - OAuth state parameter passed to the
133 |  * redirect uri. This parameter may be used for identifying the user who
134 |  * initiated the request.
135 |  * @param {Object} [options.makeBypass] - An optional parameter that allows
136 |  * users to bypass the car brand selection screen.
137 |  * For a complete list of supported makes, please see our
138 |  * [API Reference](https://smartcar.com/docs/api#authorization) documentation.
139 |  * @param {Object} [options.flags] - Object of flags where key is the name of the flag
140 |  * value is string or boolean value.
141 |  * @param {String} [options.user] - An optional unique identifier for a vehicle owner.
142 |  * This identifier is used to aggregate analytics across Connect sessions for each vehicle owner.
143 |  *
144 |  * @return {String} Smartcar Connect URL to direct user to.
145 |  * @example
146 |  * https://connect.smartcar.com/oauth/authorize?
147 |  * response_type=code
148 |  * &client_id=8229df9f-91a0-4ff0-a1ae-a1f38ee24d07
149 |  * &scope=read_odometer read_vehicle_info
150 |  * &redirect_uri=https://example.com/home
151 |  * &state=0facda3319
152 |  * &make=TESLA
153 |  * &single_select=true
154 |  * &single_select_vin=5YJSA1E14FF101307
155 |  * &flags=country:DE color:00819D
156 |  * &user=61a3e3d2-5198-47ba-aabd-4623ce4a4042
157 |  */
158 | AuthClient.prototype.getAuthUrl = function(scope, options = {}) {
159 |   /* eslint-disable camelcase */
160 |   const parameters = {
161 |     response_type: 'code',
162 |     client_id: this.clientId,
163 |     redirect_uri: this.redirectUri,
164 |     approval_prompt: options.forcePrompt === true ? 'force' : 'auto',
165 |   };
166 |   /* eslint-enable camelcase */
167 | 
168 |   parameters.scope = scope.join(' ');
169 | 
170 |   if (options.state) {
171 |     parameters.state = options.state;
172 |   }
173 | 
174 |   if (options.singleSelect) {
175 |     /* eslint-disable camelcase */
176 |     if (options.singleSelect.vin) {
177 |       parameters.single_select = true;
178 |       parameters.single_select_vin = options.singleSelect.vin;
179 |     } else if ([true, false].includes(options.singleSelect.enabled)) {
180 |       parameters.single_select = options.singleSelect.enabled;
181 |     }
182 |     /* eslint-enable camelcase */
183 |   }
184 | 
185 |   if (options.makeBypass) {
186 |     parameters.make = options.makeBypass;
187 |   }
188 | 
189 |   if (options.flags) {
190 |     parameters.flags = util.getFlagsString(options.flags);
191 |   }
192 | 
193 |   if (options.user) {
194 |     parameters.user = options.user;
195 |   }
196 | 
197 |   parameters.mode = this.mode;
198 | 
199 |   const query = qs.stringify(parameters);
200 | 
201 | 
202 |   return `${util.getConfig('SMARTCAR_API_ORIGIN') || config.connect
203 |   }/oauth/authorize?${query}`;
204 | };
205 | 
206 | /**
207 |  * Exchange an authorization code for an access object.
208 |  *
209 |  * @param {String} code - Authorization code to exchange for a Smartcar
210 |  * access token and refresh token.
211 |  * @param {Object} [options.flags] - Object of flags where key is the name of the flag
212 |  * value is string or boolean value.
213 |  * @return {Access} New set of Access and Refresh tokens.
214 |  * @throws {SmartcarError} - an instance of SmartcarError.
215 |  * See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
216 |  * for all possible errors.
217 |  */
218 | AuthClient.prototype.exchangeCode = async function(code, options = {}) {
219 |   const qs = {};
220 |   if (options.flags) {
221 |     qs.flags = util.getFlagsString(options.flags);
222 |   }
223 | 
224 |   /* eslint-disable camelcase */
225 |   const form = {
226 |     code,
227 |     grant_type: 'authorization_code',
228 |     redirect_uri: this.redirectUri,
229 |   };
230 |   /* eslint-enable camelcase */
231 | 
232 |   const response = await this.service.request(
233 |     'post',
234 |     '/oauth/token',
235 |     form,
236 |     {qs},
237 |   );
238 |   return util.formatAccess(response);
239 | };
240 | 
241 | /**
242 |  * Exchange a refresh token for a new access object.
243 |  *
244 |  * @param {String} token - Refresh token to exchange for a new set of Access and
245 |  * Refresh tokens.
246 |  * @param {Object} [options.flags] - Object of flags where key is the name of the flag
247 |  * value is string or boolean value.
248 |  * @return {Access} New set of Access and Refresh tokens.
249 |  * @throws {SmartcarError} - an instance of SmartcarError.
250 |  * See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
251 |  * for all possible errors.
252 |  */
253 | AuthClient.prototype.exchangeRefreshToken = async function(
254 |   token,
255 |   options = {},
256 | ) {
257 |   const qs = {};
258 |   if (options.flags) {
259 |     qs.flags = util.getFlagsString(options.flags);
260 |   }
261 | 
262 |   /* eslint-disable camelcase */
263 |   const form = {
264 |     grant_type: 'refresh_token',
265 |     refresh_token: token,
266 |   };
267 |   /* eslint-enable camelcase */
268 | 
269 |   const response = await this.service.request(
270 |     'post',
271 |     '/oauth/token',
272 |     form,
273 |     {qs},
274 |   );
275 |   return util.formatAccess(response);
276 | };
277 | 
278 | module.exports = AuthClient;
279 | 
280 |
281 |
282 | 283 | 284 | 285 | 286 |
287 | 288 | 291 | 292 |
293 | 294 |
295 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 296 |
297 | 298 | 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /doc/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /doc/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p 2 | 3 | 4 | 5 | JSDoc: Source: index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: index.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/* eslint-disable max-len */
 30 | /* eslint-disable camelcase */
 31 | 'use strict';
 32 | 
 33 | const _ = require('lodash');
 34 | const crypto = require('crypto');
 35 | 
 36 | const {emitWarning} = require('process');
 37 | const SmartcarService = require('./lib/smartcar-service');
 38 | const util = require('./lib/util');
 39 | const config = require('./lib/config.json');
 40 | 
 41 | /* eslint-disable global-require */
 42 | /** @exports smartcar */
 43 | const smartcar = {
 44 |   /** @see {@link module:errors} */
 45 |   SmartcarError: require('./lib/smartcar-error'),
 46 |   /** @see {@link Vehicle} */
 47 |   Vehicle: require('./lib/vehicle'),
 48 |   /** @see {@link AuthClient}*/
 49 |   AuthClient: require('./lib/auth-client'),
 50 | };
 51 | /* eslint-enable global-require */
 52 | 
 53 | const buildQueryParams = function(vin, scope, country, options) {
 54 |   const parameters = {
 55 |     vin,
 56 |     scope: scope.join(' '),
 57 |     country,
 58 |   };
 59 |   if (options.flags) {
 60 |     parameters.flags = util.getFlagsString(options.flags);
 61 |   }
 62 |   if (options.hasOwnProperty('testMode')) {
 63 |     emitWarning(
 64 |       // eslint-disable-next-line max-len
 65 |       'The "testMode" parameter is deprecated, please use the "mode" parameter instead.',
 66 |     );
 67 |     parameters.mode = options.testMode === true ? 'test' : 'live';
 68 |   } else if (options.hasOwnProperty('mode')) {
 69 |     parameters.mode = options.mode;
 70 |     if (!['test', 'live', 'simulated'].includes(parameters.mode)) {
 71 |       throw new Error( // eslint-disable-next-line max-len
 72 |         "The \"mode\" parameter MUST be one of the following: 'test', 'live', 'simulated'",
 73 |       );
 74 |     }
 75 |   }
 76 |   if (options.testModeCompatibilityLevel) {
 77 |     // eslint-disable-next-line camelcase
 78 |     parameters.test_mode_compatibility_level =
 79 |       options.testModeCompatibilityLevel;
 80 |     parameters.mode = 'test';
 81 |   }
 82 | 
 83 |   return parameters;
 84 | };
 85 | 
 86 | /**
 87 |  * Sets the version of Smartcar API you are using
 88 |  * @method
 89 |  * @param {String} version
 90 |  */
 91 | smartcar.setApiVersion = function(version) {
 92 |   config.version = version;
 93 | };
 94 | 
 95 | /**
 96 |  * Gets the version of Smartcar API that is set
 97 |  * @method
 98 |  * @return {String} version
 99 |  */
100 | smartcar.getApiVersion = () => config.version;
101 | 
102 | /**
103 |  * @type {Object}
104 |  * @typedef User
105 |  * @property {String} id - User Id
106 |  * @property {module:smartcar.Vehicle.Meta} meta
107 |  *
108 |  * @example
109 |  * {
110 |  *   id: "e0514ef4-5226-11e8-8c13-8f6e8f02e27e",
111 |  *   meta: {
112 |  *     requestId: 'b9593682-8515-4f36-8190-bb56cde4c38a',
113 |  *   }
114 |  * }
115 |  */
116 | 
117 | /**
118 |  * Return the user's id.
119 |  *
120 |  * @method
121 |  * @param {String} accessToken - access token
122 |  * @return {module:smartcar~User}
123 |  * @throws {SmartcarError} - an instance of SmartcarError.
124 |  *   See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
125 |  *   for all possible errors.
126 |  */
127 | smartcar.getUser = async function(accessToken) {
128 |   const response = await new SmartcarService({
129 |     baseUrl: util.getConfig('SMARTCAR_API_ORIGIN') || config.api,
130 |     headers: {Authorization: `Bearer ${accessToken}`},
131 |   }).request('get', `v${config.version}/user/`);
132 |   return response;
133 | };
134 | 
135 | /**
136 |  * @type {Object}
137 |  * @typedef VehicleIds
138 |  * @property {String[]} vehicles - A list of the user's authorized vehicle ids.
139 |  * @property {Object} paging
140 |  * @property {Number} paging.count- The total number of vehicles.
141 |  * @property {Number} paging.offset - The current start index of returned
142 |  * vehicle ids.
143 |  * @property {module:smartcar.Vehicle.Meta} meta
144 |  *
145 |  * @example
146 |  * {
147 |  *   vehicles: [
148 |  *     '36ab27d0-fd9d-4455-823a-ce30af709ffc',
149 |  *     '770bdda4-2429-4b20-87fd-6af475c4365e',
150 |  *   ],
151 |  *   paging: {
152 |  *     count: 2,
153 |  *     offset: 0,
154 |  *   },
155 |  *   meta: {
156 |  *     requestId: 'b9593682-8515-4f36-8190-bb56cde4c38a',
157 |  *   }
158 |  * }
159 |  */
160 | 
161 | /**
162 |  * Return list of the user's vehicles ids.
163 |  *
164 |  * @method
165 |  * @param {String} accessToken - access token
166 |  * @param {Object} [paging]
167 |  * @param {Number} [paging.limit] - number of vehicles to return
168 |  * @param {Number} [paging.offset] - index to start vehicle list
169 |  * @return {module:smartcar~VehicleIds}
170 |  * @throws {SmartcarError} - an instance of SmartcarError.
171 |  *   See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
172 |  *   for all possible errors.
173 |  */
174 | smartcar.getVehicles = async function(accessToken, paging = {}) {
175 |   const response = await new SmartcarService({
176 |     baseUrl: util.getUrl(),
177 |     headers: {Authorization: `Bearer ${accessToken}`},
178 |     qs: paging,
179 |   }).request('get', '');
180 |   return response;
181 | };
182 | 
183 | /**
184 |  * @type {Object}
185 |  * @typedef Compatibility
186 |  * @property {Boolean} compatible
187 |  * @property {(VEHICLE_NOT_COMPATIBLE|MAKE_NOT_COMPATIBLE|null)} reason
188 |  * @property {Array.<String>} capabilities
189 |  * @property {String} capabilities[].permission
190 |  * @property {String} capabilities[].endpoint
191 |  * @property {Boolean} capabilities[].capable
192 |  * @property {(VEHICLE_NOT_COMPATIBLE|MAKE_NOT_COMPATIBLE|null)} capabilities[].reason
193 |  * @property {module:smartcar.Vehicle.Meta} meta
194 |  *
195 |  * @example
196 |  * {
197 |  *  compatible: true,
198 |  *  reason: null,
199 |  *  capabilities: [
200 |  *    {
201 |  *      capable: false,
202 |  *      endpoint: '/engine/oil',
203 |  *      permission: 'read_engine_oil',
204 |  *      reason: 'SMARTCAR_NOT_CAPABLE',
205 |  *    },
206 |  *    {
207 |  *      capable: true,
208 |  *      endpoint: '/vin',
209 |  *      permission: 'read_vin',
210 |  *      reason: null,
211 |  *    },
212 |  *  ],
213 |  *  meta: {
214 |  *    'requestId':'6d4226e7-a7dd-44e0-b29c-9eed26be249d'
215 |  *  }
216 |  * }
217 |  */
218 | 
219 | /**
220 |  * Determine whether a vehicle is compatible with Smartcar.
221 |  *
222 |  * A compatible vehicle is a vehicle that:
223 |  * 1. has the hardware required for internet connectivity,
224 |  * 2. belongs to the makes and models Smartcar supports, and
225 |  * 3. supports the permissions.
226 |  *
227 |  * _To use this function, please contact us!_
228 |  *
229 |  * @param {String} vin - the VIN of the vehicle
230 |  * @param {String[]} scope - list of permissions to check compatibility for
231 |  * @param {String} [country='US'] - an optional country code according to [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
232 |  * @param {Object} [options]
233 |  * @param {Boolean} [options.testMode] - Deprecated, please use `mode` instead.
234 |  * Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
235 |  * @param {String} [options.mode] - Determine what mode Smartcar Connect should be
236 |  * launched in. Should be one of test, live or simulated.
237 |  * @param {String} [options.testModeCompatibilityLevel] - This string determines which permissions
238 |  * the simulated vehicle is capable of. Possible Values can be found at this link:
239 |  * (https://smartcar.com/docs/integration-guide/test-your-integration/test-requests/#test-successful-api-requests-with-specific-vins)
240 |  * @return {module:smartcar~Compatibility}
241 |  * @throws {SmartcarError} - an instance of SmartcarError.
242 |  *   See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
243 |  *   for all possible errors.
244 |  */
245 | smartcar.getCompatibility = async function(vin, scope, country, options = {}) {
246 |   country = country || 'US';
247 |   const clientId =
248 |     options.clientId || util.getOrThrowConfig('SMARTCAR_CLIENT_ID');
249 |   const clientSecret =
250 |     options.clientSecret || util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET');
251 | 
252 |   const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
253 | 
254 |   const response = await new SmartcarService({
255 |     baseUrl: util.getConfig('SMARTCAR_API_ORIGIN') || config.api,
256 |     headers: {
257 |       Authorization: `Basic ${credentials}`,
258 |     },
259 |     qs: buildQueryParams(vin, scope, country, options),
260 |   }).request('get', `v${options.version || config.version}/compatibility`);
261 |   return response;
262 | };
263 | 
264 | /**
265 |  * Generate hash challenege for webhooks. It does HMAC_SHA256(amt, challenge)
266 |  *
267 |  * @method
268 |  * @param {String} amt - Application Management Token
269 |  * @param {String} challenge - Challenge string
270 |  * @return {String}  String representing the hex digest
271 |  */
272 | smartcar.hashChallenge = function(amt, challenge) {
273 |   const hmac = crypto.createHmac('sha256', amt);
274 |   return hmac.update(challenge).digest('hex');
275 | };
276 | 
277 | /**
278 |  * Verify webhook payload with AMT and signature.
279 |  *
280 |  * @method
281 |  * @param {String} amt - Application Management Token
282 |  * @param {String} signature - sc-signature header value
283 |  * @param {object} body - webhook response body
284 |  * @return {Boolean} true if signature matches the hex digest of amt and body
285 |  */
286 | smartcar.verifyPayload = (amt, signature, body) =>
287 |   smartcar.hashChallenge(amt, JSON.stringify(body)) === signature;
288 | 
289 | /**
290 |  * Returns a paged list of all the vehicles that are connected to the application associated
291 |  * with the management API token used sorted in descending order by connection date.
292 |  *
293 |  * @type {Object}
294 |  * @typedef Connection
295 |  * @property {String} vehicleId
296 |  * @property {String} userId
297 |  * @property {String} connectedAt
298 |  *
299 |  * @type {Object}
300 |  * @typedef GetConnections
301 |  * @property {Connection[]} connections
302 |  * @property {Object} [paging]
303 |  * @property {string} [paging.cursor]
304 |  *
305 |  * @param {String} amt - Application Management Token
306 |  * @param {object} filter
307 |  * @param {String} filter.userId
308 |  * @param {String} filter.vehicleId
309 |  * @param {object} paging
310 |  * @param {number} paging.limit
311 |  * @param {String} paging.cursor
312 |  * @returns {GetConnections}
313 |  */
314 | smartcar.getConnections = async function(amt, filter = {}, paging = {}) {
315 |   const {userId, vehicleId} = _.pick(filter, ['userId', 'vehicleId']);
316 |   const {limit, cursor} = _.pick(paging, ['limit', 'cursor']);
317 | 
318 |   const credentials = Buffer.from(`default:${amt}`).toString('base64');
319 | 
320 |   const qs = {};
321 |   if (userId) {
322 |     qs.user_id = userId;
323 |   }
324 |   if (vehicleId) {
325 |     qs.vehicle_id = vehicleId;
326 |   }
327 |   if (limit) {
328 |     qs.limit = limit;
329 |   }
330 |   // istanbul ignore next
331 |   if (cursor) {
332 |     qs.cursor = cursor;
333 |   }
334 | 
335 |   const response = await new SmartcarService({
336 |     // eslint-disable-next-line max-len
337 |     baseUrl:
338 |       `${util.getConfig('SMARTCAR_MANAGEMENT_API_ORIGIN') || config.management}/v${config.version}`,
339 |     headers: {
340 |       Authorization: `Basic ${credentials}`,
341 |     },
342 |     qs,
343 |   }).request('get', '/management/connections');
344 | 
345 |   return response;
346 | };
347 | 
348 | /**
349 |  * Deletes all the connections by vehicle or user ID and returns a
350 |  * list of all connections that were deleted.
351 |  *
352 |  * @type {Object}
353 |  * @typedef Connection
354 |  * @property {String} vehicleId
355 |  * @property {String} userId
356 |  *
357 |  * @type {Object}
358 |  * @typedef DeleteConnections
359 |  * @property {Connection[]} connections
360 |  *
361 |  * @param {String} amt - Application Management Token
362 |  * @param {object} filter
363 |  * @param {String} filter.userId
364 |  * @param {String} filter.vehicleId
365 |  * @returns {DeleteConnections}
366 |  */
367 | smartcar.deleteConnections = async function(amt, filter) {
368 |   const {userId, vehicleId} = _.pick(filter, ['userId', 'vehicleId']);
369 |   if (userId && vehicleId) {
370 |     // eslint-disable-next-line max-len
371 |     throw new Error(
372 |       'Filter can contain EITHER user_id OR vehicle_id, not both',
373 |     );
374 |   }
375 | 
376 |   const qs = {};
377 |   if (userId) {
378 |     qs.user_id = userId;
379 |   }
380 |   if (vehicleId) {
381 |     qs.vehicle_id = vehicleId;
382 |   }
383 |   const credentials = Buffer.from(`default:${amt}`).toString('base64');
384 | 
385 |   const response = await new SmartcarService({
386 |     baseUrl:
387 |       `${util.getConfig('SMARTCAR_MANAGEMENT_API_ORIGIN') || config.management}/v${config.version}`,
388 |     headers: {
389 |       Authorization: `Basic ${credentials}`,
390 |     },
391 |     qs,
392 |   }).request('delete', '/management/connections');
393 | 
394 |   return response;
395 | };
396 | 
397 | module.exports = smartcar;
398 | 
399 |
400 |
401 | 402 | 403 | 404 | 405 |
406 | 407 | 410 | 411 |
412 | 413 |
414 | Documentation generated by JSDoc 3.6.10 on Mon Sep 22 2025 10:17:59 GMT-0700 (Pacific Daylight Time) 415 |
416 | 417 | 418 | 419 | 420 | 421 | -------------------------------------------------------------------------------- /test/unit/lib/vehicle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nock = require('nock'); 4 | const test = require('ava'); 5 | const sinon = require('sinon'); 6 | 7 | const Vehicle = require('../../../lib/vehicle'); 8 | const {USER_AGENT} = require('../../../lib/util'); 9 | 10 | const VID = 'ada7207c-3c0a-4027-a47f-6215ce6f7b93'; 11 | const TOKEN = '9ad942c6-32b8-4af2-ada6-5e8ecdbad9c2'; 12 | 13 | const vehicle = new Vehicle(VID, TOKEN); 14 | 15 | const v3SignalsResponse = { 16 | data: [ 17 | { 18 | id: '', 19 | type: 'signal', 20 | attributes: { 21 | code: 'charge-voltage', 22 | name: 'Voltage', 23 | group: 'Charge', 24 | status: { 25 | value: 'SUCCESS', 26 | }, 27 | body: { 28 | unit: 'volts', 29 | value: 85, 30 | }, 31 | }, 32 | meta: { 33 | retrievedAt: 1696156800, 34 | oemUpdatedAt: 1696156799, 35 | }, 36 | links: { 37 | self: '', 38 | values: '', 39 | vehicle: '', 40 | }, 41 | }, 42 | ], 43 | meta: { 44 | page: 1, 45 | pageSize: 10, 46 | totalCount: 5, 47 | }, 48 | links: { 49 | self: '/vehicles/vehicle123/signals?page=1&pageSize=10', 50 | first: '/vehicles/vehicle123/signals?page=1&pageSize=10', 51 | previous: null, 52 | next: '/vehicles/vehicle123/signals?page=2&pageSize=10', 53 | last: '/vehicles/vehicle123/signals?page=5&pageSize=10', 54 | }, 55 | included: { 56 | vehicle: { 57 | id: '', 58 | type: 'vehicle', 59 | attributes: { 60 | make: '', 61 | model: '', 62 | year: 123, 63 | }, 64 | links: { 65 | self: '', 66 | }, 67 | }, 68 | }, 69 | }; 70 | 71 | const v3SignalResponse = { 72 | id: '', 73 | type: 'signal', 74 | attributes: { 75 | code: 'odometer-traveleddistance', 76 | name: 'TraveledDistance', 77 | group: 'Odometer', 78 | status: { 79 | value: 'SUCCESS', 80 | }, 81 | body: { 82 | unit: 'km', 83 | value: 100, 84 | }, 85 | }, 86 | meta: { 87 | retrievedAt: 1696156800, 88 | oemUpdatedAt: 1696156799, 89 | }, 90 | links: { 91 | self: '', 92 | values: '', 93 | vehicle: '', 94 | }, 95 | }; 96 | 97 | const nocks = { 98 | base(version = vehicle.version, vid = VID, token = TOKEN) { 99 | return nock(`https://api.smartcar.com/v${version}/vehicles/${vid}`) 100 | .matchHeader('User-Agent', USER_AGENT) 101 | .matchHeader('Authorization', `Bearer ${token}`); 102 | }, 103 | baseV3(vid = VID, token = TOKEN) { 104 | return nock(`https://vehicle.api.smartcar.com/v3/vehicles/${vid}`) 105 | .matchHeader('User-Agent', USER_AGENT) 106 | .matchHeader('Authorization', `Bearer ${token}`); 107 | }, 108 | }; 109 | 110 | test.afterEach(function(t) { 111 | if (t.context.n) { 112 | t.true(t.context.n.isDone()); 113 | } 114 | }); 115 | 116 | test('constructor - default parameters check', async function(t) { 117 | const vehicle = new Vehicle(VID, TOKEN); 118 | t.context.n = nocks 119 | .base(vehicle.version) 120 | .matchHeader('sc-unit-system', 'metric') 121 | .get('/default') 122 | .reply(200, '{"pizza": "pasta"}'); 123 | 124 | t.is(vehicle.id, VID); 125 | t.is(vehicle.token, TOKEN); 126 | t.is(vehicle.unitSystem, 'metric'); 127 | t.is(vehicle.version, '2.0'); 128 | 129 | const res = await vehicle.service.request('get', 'default'); 130 | t.is(res.pizza, 'pasta'); 131 | }); 132 | 133 | test('constructor - non default unit and version', async function(t) { 134 | const vehicle = new Vehicle(VID, TOKEN, { 135 | unitSystem: 'imperial', 136 | version: '4.4', 137 | }); 138 | t.context.n = nocks 139 | .base('4.4') 140 | .matchHeader('sc-unit-system', 'imperial') 141 | .get('/constructor/imperial') 142 | .reply(200, '{"pizza": "pasta"}'); 143 | 144 | t.is(vehicle.id, VID); 145 | t.is(vehicle.token, TOKEN); 146 | t.is(vehicle.unitSystem, 'imperial'); 147 | 148 | const res = await vehicle.service.request('get', '/constructor/imperial'); 149 | t.is(res.pizza, 'pasta'); 150 | }); 151 | 152 | test('constructor - with optional flags', async function(t) { 153 | const vehicle = new Vehicle(VID, TOKEN, { 154 | flags: {country: 'DE', flag: 'suboption'}, 155 | }); 156 | t.context.n = nocks 157 | .base(vehicle.version) 158 | .matchHeader('sc-unit-system', 'metric') 159 | .get('/default?flags=country%3ADE%20flag%3Asuboption') 160 | .reply(200, '{"pizza": "pasta"}'); 161 | 162 | t.deepEqual(vehicle.query, {flags: 'country:DE flag:suboption'}); 163 | const res = await vehicle.service.request('get', 'default'); 164 | t.is(res.pizza, 'pasta'); 165 | }); 166 | 167 | test('vehicle webhook subscribe', async(t) => { 168 | const responseBody = {webhookId: 'webhookID', vehicleId: 'vehicleId'}; 169 | t.context.n = nocks 170 | .base() 171 | .post('/webhooks/webhookID') 172 | .reply(200, responseBody, { 173 | 'sc-request-id': 'requestId', 174 | }); 175 | 176 | const response = await vehicle.subscribe('webhookID'); 177 | 178 | t.is(response.webhookId, 'webhookID'); 179 | t.is(response.vehicleId, 'vehicleId'); 180 | t.is(response.meta.requestId, 'requestId'); 181 | }); 182 | 183 | test('vehicle webhook unsubscribe', async(t) => { 184 | t.context.n = nocks 185 | .base(vehicle.version, VID, 'amt') 186 | .delete('/webhooks/webhookID') 187 | .reply(200, '', { 188 | 'sc-request-id': 'requestId', 189 | }); 190 | 191 | const response = await vehicle.unsubscribe('amt', 'webhookID'); 192 | 193 | t.is(response.meta.requestId, 'requestId'); 194 | }); 195 | 196 | test('vehicle permissions', async(t) => { 197 | t.context.n = nocks 198 | .base() 199 | .get('/permissions') 200 | .query({limit: '1'}) 201 | .reply(200, {permissions: []}, { 202 | 'sc-request-id': 'requestId', 203 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 204 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 205 | }); 206 | 207 | const response = await vehicle.permissions({limit: 1}); 208 | 209 | t.is(response.meta.requestId, 'requestId'); 210 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 211 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 212 | t.is(response.permissions.length, 0); 213 | t.true(t.context.n.isDone()); 214 | }); 215 | 216 | test('batch - success', async function(t) { 217 | // We are intentionally testing multiple routes here to make sure dynamic mapping 218 | // of function name is working for all cases. Please do not remove the paths being 219 | // asserted here. 220 | const paths = [ 221 | '/', '/odometer', '/engine/oil', 'tires/pressure', 'tesla/speedometer', 222 | ]; 223 | const requestBody = { 224 | requests: [ 225 | { 226 | path: '/', 227 | }, 228 | { 229 | path: '/odometer', 230 | }, 231 | { 232 | path: '/engine/oil', 233 | }, 234 | { 235 | path: 'tires/pressure', 236 | }, 237 | { 238 | path: 'tesla/speedometer', 239 | }, 240 | ], 241 | }; 242 | const mockResponse = { 243 | responses: [ 244 | { 245 | headers: { 246 | 'sc-unit-system': 'imperial', 247 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 248 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 249 | }, 250 | path: '/', 251 | code: 200, 252 | body: { 253 | id: '36ab27d0-fd9d-4455-823a-ce30af709ffc', 254 | make: 'TESLA', 255 | model: 'Model S', 256 | year: 2014, 257 | }, 258 | }, 259 | { 260 | headers: { 261 | 'sc-unit-system': 'imperial', 262 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 263 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 264 | }, 265 | path: '/odometer', 266 | code: 200, 267 | body: { 268 | distance: 32768, 269 | }, 270 | }, 271 | { 272 | headers: {'sc-unit-system': 'imperial'}, 273 | path: '/engine/oil', 274 | code: 501, 275 | body: { 276 | error: 'vehicle_not_capable_error', 277 | message: 'Vehicle is not capable of performing request.', 278 | }, 279 | }, 280 | { 281 | headers: { 282 | 'sc-unit-system': 'imperial', 283 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 284 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 285 | }, 286 | path: '/tires/pressure', 287 | code: 200, 288 | body: { 289 | backLeft: 219.3, 290 | backRight: 219.3, 291 | frontLeft: 219.3, 292 | frontRight: 219.3, 293 | }, 294 | }, 295 | { 296 | headers: { 297 | 'sc-unit-system': 'imperial', 298 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 299 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 300 | }, 301 | path: '/tesla/speedometer', 302 | code: 200, 303 | body: { 304 | speed: 84.32, 305 | }, 306 | }, 307 | ], 308 | }; 309 | t.context.n = nocks 310 | .base() 311 | .post('/batch', requestBody) 312 | .reply(200, mockResponse, {'sc-request-id': 'requestId'}); 313 | 314 | const response = await vehicle.batch(paths); 315 | 316 | const odometer = response.odometer(); 317 | t.is(odometer.distance, 32768); 318 | t.is(odometer.meta.requestId, 'requestId'); 319 | t.is(odometer.meta.unitSystem, 'imperial'); 320 | t.is(odometer.meta.dataAge.valueOf(), 1525418450844); 321 | t.is(odometer.meta.fetchedAt.valueOf(), 1525418451844); 322 | 323 | const attributes = response.attributes(); 324 | t.is(attributes.make, 'TESLA'); 325 | t.is(attributes.model, 'Model S'); 326 | t.is(attributes.year, 2014); 327 | t.is(attributes.meta.requestId, 'requestId'); 328 | t.is(attributes.meta.unitSystem, 'imperial'); 329 | t.is(attributes.meta.dataAge.valueOf(), 1525418450844); 330 | t.is(attributes.meta.fetchedAt.valueOf(), 1525418451844); 331 | 332 | const expectedMessage = 'vehicle_not_capable_error:undefined - ' 333 | + 'Vehicle is not capable of performing request.'; 334 | const error = t.throws(() => response.engineOil()); 335 | t.is(error.message, expectedMessage); 336 | t.is(error.type, 'vehicle_not_capable_error'); 337 | 338 | const tirePressure = response.tirePressure(); 339 | t.is(tirePressure.backLeft, 219.3); 340 | t.is(tirePressure.backRight, 219.3); 341 | t.is(tirePressure.frontLeft, 219.3); 342 | t.is(tirePressure.frontRight, 219.3); 343 | t.is(attributes.meta.requestId, 'requestId'); 344 | t.is(attributes.meta.unitSystem, 'imperial'); 345 | t.is(attributes.meta.dataAge.valueOf(), 1525418450844); 346 | t.is(attributes.meta.fetchedAt.valueOf(), 1525418451844); 347 | 348 | const speedometer = response.teslaSpeedometer(); 349 | t.is(speedometer.speed, 84.32); 350 | t.is(attributes.meta.requestId, 'requestId'); 351 | t.is(attributes.meta.unitSystem, 'imperial'); 352 | t.is(attributes.meta.dataAge.valueOf(), 1525418450844); 353 | t.is(attributes.meta.fetchedAt.valueOf(), 1525418451844); 354 | }); 355 | 356 | test('batch - error', async function(t) { 357 | const paths = ['/odometer', '/engine/oil', '/location']; 358 | const requestBody = { 359 | requests: [ 360 | { 361 | path: '/odometer', 362 | }, 363 | { 364 | path: '/engine/oil', 365 | }, 366 | { 367 | path: '/location', 368 | }, 369 | ], 370 | }; 371 | 372 | t.context.n = nocks 373 | .base() 374 | .post('/batch', requestBody) 375 | .reply(500, { 376 | error: 'monkeys_on_mars', 377 | message: 'yes, really', 378 | }, { 379 | 'sc-request-id': 'requestId', 380 | }); 381 | 382 | const error = await t.throwsAsync(vehicle.batch(paths)); 383 | t.is(error.message, 'monkeys_on_mars:undefined - yes, really'); 384 | t.is(error.type, 'monkeys_on_mars'); 385 | }); 386 | 387 | test('request - override non-sc headers', async function(t) { 388 | t.context.n = nock( 389 | `https://api.smartcar.com/v${vehicle.version}/vehicles/${VID}`, 390 | ) 391 | .matchHeader('User-Agent', 'monkeys_on_mars') 392 | .matchHeader('Authorization', `Bearer ${TOKEN}`) 393 | .matchHeader('Origin', 'monkeys_on_pluto') 394 | .get('/odometer') 395 | .reply(200, {distance: 10}, { 396 | 'sc-request-id': 'requestId', 397 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 398 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 399 | }); 400 | 401 | const response = await vehicle.request('get', 'odometer', undefined, { 402 | 'User-Agent': 'monkeys_on_mars', 403 | Origin: 'monkeys_on_pluto', 404 | }); 405 | 406 | t.is(response.body.distance, 10); 407 | t.is(response.meta.requestId, 'requestId'); 408 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 409 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 410 | }); 411 | 412 | test('request - rate limit', async function(t) { 413 | const retryAfter = new Date().valueOf(); 414 | t.context.n = nock( 415 | `https://api.smartcar.com/v${vehicle.version}/vehicles/${VID}`, 416 | ) 417 | .matchHeader('Authorization', `Bearer ${TOKEN}`) 418 | .get('/odometer') 419 | .reply(429, { 420 | error: 'RATE_LIMIT', 421 | suggestedUserMessage: 'This just will not do', 422 | }, 423 | {'retry-after': retryAfter}); 424 | 425 | const error = await t.throwsAsync(vehicle.odometer()); 426 | t.is(error.retryAfter, String(retryAfter)); 427 | t.is(error.suggestedUserMessage, 'This just will not do'); 428 | }); 429 | 430 | test('request - get charge limit', async function(t) { 431 | sinon.restore(); // clear all spys 432 | 433 | t.context.n = nocks 434 | .base() 435 | .get('/charge/limit') 436 | .reply(200, {limit: 0.7}, {'sc-request-id': 'requestId'}); 437 | 438 | const serviceRequestSpy = sinon.spy(vehicle.service, 'request'); 439 | 440 | const response = await vehicle.getChargeLimit(); 441 | 442 | t.true(serviceRequestSpy.calledOnceWith('get', 'charge/limit')); 443 | t.is(response.meta.requestId, 'requestId'); 444 | t.is(response.limit, 0.7); 445 | t.true(t.context.n.isDone()); 446 | }); 447 | 448 | test('request - v3 signals', async function(t) { 449 | sinon.restore(); // clear all spys 450 | 451 | t.context.n = nocks 452 | .baseV3() 453 | .get('/signals') 454 | .reply(200, v3SignalsResponse); 455 | 456 | const serviceRequestSpy = sinon.spy(vehicle, 'request'); 457 | 458 | const response = await vehicle.getSignals(); 459 | 460 | t.true(serviceRequestSpy.calledOnceWith('get', 'signals')); 461 | t.deepEqual(response.body.data, v3SignalsResponse.data); 462 | }); 463 | 464 | test('request - v3 signal odometer-traveleddistance', async function(t) { 465 | sinon.restore(); // clear all spys 466 | 467 | const signalCode = 'odometer-traveleddistance'; 468 | t.context.n = nocks 469 | .baseV3() 470 | .get(`/signals/${signalCode}`) 471 | .reply(200, v3SignalResponse); 472 | 473 | const serviceRequestSpy = sinon.spy(vehicle, 'request'); 474 | 475 | const response = await vehicle.getSignal(signalCode); 476 | 477 | t.true(serviceRequestSpy.calledOnceWith('get', `signals/${signalCode}`)); 478 | t.deepEqual(response.body.data, v3SignalResponse.data); 479 | }); 480 | 481 | test('request - set charge limit', async function(t) { 482 | sinon.restore(); // clear all spys 483 | 484 | t.context.n = nocks 485 | .base() 486 | .post('/charge/limit') 487 | .reply(200, {}, {'sc-request-id': 'requestId'}); 488 | 489 | const chargeLimit = 0.6; 490 | const serviceRequestSpy = sinon.spy(vehicle.service, 'request'); 491 | const response = await vehicle.setChargeLimit(chargeLimit); 492 | 493 | t.is(response.meta.requestId, 'requestId'); 494 | t.true(serviceRequestSpy.calledOnce); 495 | t.true( 496 | serviceRequestSpy.calledWith( 497 | 'post', 'charge/limit', sinon.match({limit: String(chargeLimit)}), 498 | ), 499 | ); 500 | t.true(t.context.n.isDone()); 501 | }); 502 | 503 | test('request - security', async function(t) { 504 | sinon.restore(); // clear all spys 505 | 506 | t.context.n = nocks 507 | .base() 508 | .get('/security') 509 | .reply(200, 510 | { 511 | isLocked: true, 512 | doors: [], 513 | windows: [], 514 | storage: [], 515 | sunroof: [], 516 | chargingPort: [], 517 | }, { 518 | 'sc-request-id': 'requestId', 519 | 'sc-data-age': '2018-05-04T07:20:50.844Z', 520 | 'sc-fetched-at': '2018-05-04T07:20:51.844Z', 521 | }); 522 | 523 | const serviceRequestSpy = sinon.spy(vehicle.service, 'request'); 524 | 525 | const response = await vehicle.lockStatus(); 526 | 527 | t.true(serviceRequestSpy.calledOnceWith('get', 'security')); 528 | t.is(response.meta.requestId, 'requestId'); 529 | t.is(response.meta.dataAge.valueOf(), 1525418450844); 530 | t.is(response.meta.fetchedAt.valueOf(), 1525418451844); 531 | t.is(response.isLocked, true); 532 | t.true(t.context.n.isDone()); 533 | }); 534 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const nock = require('nock'); 5 | 6 | const smartcar = require('../../'); 7 | const SmartcarService = require('../../lib/smartcar-service'); 8 | const SmartcarError = require('../../lib/smartcar-error'); 9 | 10 | const v3VehiclesResponse = { 11 | id: 'vehicleId123', 12 | type: 'vehicle', 13 | attributes: { 14 | make: '', 15 | model: '', 16 | year: 123, 17 | }, 18 | links: { 19 | self: '', 20 | }, 21 | }; 22 | 23 | test('setApiVersion and getApiVersion', function(t) { 24 | let vehicle = new smartcar.Vehicle(); 25 | t.is(vehicle.version, '2.0'); 26 | 27 | smartcar.setApiVersion('4.0'); 28 | t.is(smartcar.getApiVersion(), '4.0'); 29 | // checking the old vehicle object has same version as before 30 | t.is(vehicle.version, '2.0'); 31 | 32 | vehicle = new smartcar.Vehicle(); 33 | t.is(vehicle.version, '4.0'); 34 | 35 | smartcar.setApiVersion('2.0'); 36 | t.is(vehicle.version, '4.0'); 37 | 38 | vehicle = new smartcar.Vehicle(); 39 | t.is(vehicle.version, '2.0'); 40 | }); 41 | 42 | test('hashChallenge', function(t) { 43 | const res = smartcar.hashChallenge('amt', 'challenge'); 44 | t.is(res, '9baf5a7464bd86740ad5a06e439dcf535a075022ed2c92d74efacf646d79328e'); 45 | }); 46 | 47 | test('verifyPayload', function(t) { 48 | // eslint-disable-next-line max-len 49 | const signature = '4c05a8da471f05156ad717baa4017acd13a3a809850b9ca7d3301dcaaa854f70'; 50 | const res = smartcar.verifyPayload('amt', signature, {pizza: 'pasta'}); 51 | t.true(res); 52 | }); 53 | 54 | test('getVehicles - simple', async function(t) { 55 | 56 | const n = nock('https://api.smartcar.com/v2.0/') 57 | .get('/vehicles/') 58 | .matchHeader('Authorization', 'Bearer simple') 59 | .reply(200, { 60 | vehicles: ['vehicle1', 'vehicle2', 'vehicle3'], 61 | paging: {count: 3, offset: 0}, 62 | }); 63 | 64 | const res = await smartcar.getVehicles('simple'); 65 | t.is(res.vehicles.length, 3); 66 | t.true(n.isDone()); 67 | }); 68 | 69 | test('getVehicles - paging', async function(t) { 70 | 71 | const n = nock('https://api.smartcar.com/v2.0/') 72 | .get('/vehicles/') 73 | .query({limit: '1'}) 74 | .matchHeader('Authorization', 'Bearer token') 75 | .reply(200, { 76 | vehicles: ['vehicle1'], 77 | paging: {count: 1, offset: 0}, 78 | }); 79 | 80 | const res = await smartcar.getVehicles('token', {limit: 1}); 81 | t.is(res.vehicles.length, 1); 82 | t.true(n.isDone()); 83 | }); 84 | 85 | test('getVehicle - v3', async function(t) { 86 | 87 | const vehicle = new smartcar.Vehicle(); 88 | vehicle.id = v3VehiclesResponse.id; 89 | 90 | const n = nock('https://vehicle.api.smartcar.com/v3/') 91 | .get(`/vehicles/${vehicle.id}/`) 92 | .matchHeader('Authorization', 'Bearer simple') 93 | .reply(200, v3VehiclesResponse); 94 | 95 | const res = await smartcar.getVehicle('simple', vehicle.id); 96 | t.is(res.body.id, v3VehiclesResponse.id); 97 | t.true(n.isDone()); 98 | }); 99 | 100 | test('getUser', async function(t) { 101 | const n = nock('https://api.smartcar.com/v2.0/') 102 | .get('/user/') 103 | .matchHeader('Authorization', 'Bearer token') 104 | .reply(200, { 105 | id: 'userid', 106 | }); 107 | 108 | const response = await smartcar.getUser('token'); 109 | t.is(response.id, 'userid'); 110 | t.true(n.isDone()); 111 | }); 112 | 113 | test('exports', function(t) { 114 | t.true('SmartcarError' in smartcar); 115 | t.true('Vehicle' in smartcar); 116 | t.true('AuthClient' in smartcar); 117 | }); 118 | 119 | test('getCompatibility - client id and secret errors', async function(t) { 120 | const vin = 'fake_vin'; 121 | const scope = ['read_location', 'read_odometer']; 122 | 123 | 124 | let error = await t.throwsAsync(smartcar.getCompatibility(vin, scope)); 125 | t.is(error.message, 'SMARTCAR_CLIENT_ID not set or passed as arguments'); 126 | 127 | error = await t.throwsAsync( 128 | smartcar.getCompatibility(vin, scope, 'US', {clientId: 'clientId'}), 129 | ); 130 | t.is(error.message, 'SMARTCAR_CLIENT_SECRET not set or passed as arguments'); 131 | }); 132 | 133 | test('getCompatibility - with flags, testModeCompatibilityLevel and override version', async function(t) { 134 | const vin = 'fake_vin'; 135 | const scope = ['read_location', 'read_odometer']; 136 | const path = '/compatibility?vin=fake_vin&' 137 | + 'scope=read_location%20read_odometer&country=US&' 138 | + 'flags=test%3Atest&test_mode_compatibility_level=pizza&mode=test'; 139 | const n = nock('https://api.smartcar.com/v6.6/') 140 | .get(path) 141 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 142 | .reply(200, { 143 | pizza: 'pasta', 144 | }); 145 | 146 | const response = await smartcar.getCompatibility(vin, scope, 'US', { 147 | clientId: 'clientId', 148 | clientSecret: 'clientSecret', 149 | version: '6.6', 150 | flags: {test: 'test'}, 151 | testModeCompatibilityLevel: 'pizza', 152 | }); 153 | 154 | t.is(response.pizza, 'pasta'); 155 | t.true(n.isDone()); 156 | }); 157 | 158 | test('getCompatibility - No country provided', async function(t) { 159 | const vin = 'fake_vin'; 160 | const scope = ['read_location', 'read_odometer']; 161 | const path = '/compatibility?vin=fake_vin&' 162 | + 'scope=read_location%20read_odometer&country=US&' 163 | + 'flags=test%3Atest&test_mode_compatibility_level=pizza&mode=test'; 164 | const n = nock('https://api.smartcar.com/v6.6/') 165 | .get(path) 166 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 167 | .reply(200, { 168 | pizza: 'pasta', 169 | }); 170 | 171 | const response = await smartcar.getCompatibility(vin, scope, { 172 | clientId: 'clientId', 173 | clientSecret: 'clientSecret', 174 | version: '6.6', 175 | flags: {test: 'test'}, 176 | testModeCompatibilityLevel: 'pizza', 177 | }); 178 | 179 | t.is(response.pizza, 'pasta'); 180 | t.true(n.isDone()); 181 | }); 182 | 183 | test('getCompatibility - mode invalid input errors', async function(t) { 184 | const vin = 'fake_vin'; 185 | const scope = ['read_location', 'read_odometer']; 186 | 187 | 188 | const err = await t.throwsAsync(smartcar.getCompatibility(vin, scope, 'US', { 189 | clientId: 'clientId', 190 | clientSecret: 'clientSecret', 191 | version: '6.6', 192 | mode: 'pizzapasta', 193 | })); 194 | t.is( 195 | err.message, 196 | // eslint-disable-next-line max-len 197 | 'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'', 198 | ); 199 | }); 200 | 201 | test('getCompatibility - with mode simulated', async function(t) { 202 | const vin = 'fake_vin'; 203 | const scope = ['read_location', 'read_odometer']; 204 | const path = '/compatibility?vin=fake_vin&' 205 | + 'scope=read_location%20read_odometer&country=US&' 206 | + 'mode=simulated'; 207 | const n = nock('https://api.smartcar.com/v6.6/') 208 | .get(path) 209 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 210 | .reply(200, { 211 | pizza: 'pasta', 212 | }); 213 | 214 | const response = await smartcar.getCompatibility(vin, scope, 'US', { 215 | clientId: 'clientId', 216 | clientSecret: 'clientSecret', 217 | version: '6.6', 218 | mode: 'simulated', 219 | }); 220 | 221 | t.is(response.pizza, 'pasta'); 222 | t.true(n.isDone()); 223 | }); 224 | 225 | test('getCompatibility - with test_mode true [deprecated]', async function(t) { 226 | const vin = 'fake_vin'; 227 | const scope = ['read_location', 'read_odometer']; 228 | const path = '/compatibility?vin=fake_vin&' 229 | + 'scope=read_location%20read_odometer&country=US&' 230 | + 'mode=test'; 231 | const n = nock('https://api.smartcar.com/v6.6/') 232 | .get(path) 233 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 234 | .reply(200, { 235 | pizza: 'pasta', 236 | }); 237 | 238 | const response = await smartcar.getCompatibility(vin, scope, 'US', { 239 | clientId: 'clientId', 240 | clientSecret: 'clientSecret', 241 | version: '6.6', 242 | testMode: true, 243 | }); 244 | 245 | t.is(response.pizza, 'pasta'); 246 | t.true(n.isDone()); 247 | }); 248 | 249 | test('getCompatibility - with test_mode false [deprecated]', async function(t) { 250 | const vin = 'fake_vin'; 251 | const scope = ['read_location', 'read_odometer']; 252 | const path = '/compatibility?vin=fake_vin&' 253 | + 'scope=read_location%20read_odometer&country=US&' 254 | + 'mode=live'; 255 | const n = nock('https://api.smartcar.com/v6.6/') 256 | .get(path) 257 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 258 | .reply(200, { 259 | pizza: 'pasta', 260 | }); 261 | 262 | const response = await smartcar.getCompatibility(vin, scope, 'US', { 263 | clientId: 'clientId', 264 | clientSecret: 'clientSecret', 265 | version: '6.6', 266 | testMode: false, 267 | }); 268 | 269 | t.is(response.pizza, 'pasta'); 270 | t.true(n.isDone()); 271 | }); 272 | 273 | test('getCompatibilityMatrix ', async function(t) { 274 | const path = '/compatibility/matrix?mode=live®ion=US'; 275 | const n = nock('https://api.smartcar.com/v6.6/') 276 | .get(path) 277 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 278 | .reply(200, { 279 | pizza: 'pasta', 280 | }); 281 | 282 | const response = await smartcar.getCompatibilityMatrix('US', { 283 | clientId: 'clientId', 284 | clientSecret: 'clientSecret', 285 | version: '6.6', 286 | testMode: false, 287 | }); 288 | 289 | t.is(response.pizza, 'pasta'); 290 | t.true(n.isDone()); 291 | }); 292 | 293 | test('getCompatibilityMatrix - with options', async function(t) { 294 | const type = 'BEV'; 295 | const make = 'TESLA'; 296 | // eslint-disable-next-line max-len 297 | const path = `/compatibility/matrix?mode=live®ion=US&type=${type}&make=${make}`; 298 | const n = nock('https://api.smartcar.com/v6.6/') 299 | .get(path) 300 | .matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0') 301 | .reply(200, { 302 | pizza: 'pasta', 303 | }); 304 | 305 | const response = await smartcar.getCompatibilityMatrix('US', { 306 | clientId: 'clientId', 307 | clientSecret: 'clientSecret', 308 | version: '6.6', 309 | testMode: false, 310 | make, 311 | type, 312 | }); 313 | 314 | t.is(response.pizza, 'pasta'); 315 | t.true(n.isDone()); 316 | }); 317 | 318 | test('getConnections - no filters', async function(t) { 319 | const n = nock('https://management.smartcar.com/v2.0/') 320 | .get('/management/connections') 321 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 322 | .reply(200, { 323 | connections: [ 324 | {vehicleId: 'vehicle1', userId: 'user1', connectedAt: '2023-01-01'}, 325 | {vehicleId: 'vehicle2', userId: 'user2', connectedAt: '2023-01-02'}, 326 | ], 327 | }); 328 | 329 | const response = await smartcar.getConnections('fake-amt'); 330 | t.is(response.connections.length, 2); 331 | t.is(response.connections[0].vehicleId, 'vehicle1'); 332 | t.true(n.isDone()); 333 | }); 334 | 335 | test('getConnections - with userId filter', async function(t) { 336 | const n = nock('https://management.smartcar.com/v2.0/') 337 | .get('/management/connections') 338 | // eslint-disable-next-line camelcase 339 | .query({user_id: 'test-user-123'}) 340 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 341 | .reply(200, { 342 | connections: [ 343 | { 344 | vehicleId: 'vehicle1', 345 | userId: 'test-user-123', 346 | connectedAt: '2023-01-01', 347 | }, 348 | ], 349 | }); 350 | 351 | const response = await smartcar.getConnections('fake-amt', { 352 | userId: 'test-user-123', 353 | }); 354 | t.is(response.connections.length, 1); 355 | t.is(response.connections[0].userId, 'test-user-123'); 356 | t.true(n.isDone()); 357 | }); 358 | 359 | test('getConnections - with vehicleId filter', async function(t) { 360 | const n = nock('https://management.smartcar.com/v2.0/') 361 | .get('/management/connections') 362 | // eslint-disable-next-line camelcase 363 | .query({vehicle_id: 'test-vehicle-456'}) 364 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 365 | .reply(200, { 366 | connections: [ 367 | { 368 | vehicleId: 'test-vehicle-456', 369 | userId: 'user1', 370 | connectedAt: '2023-01-01', 371 | }, 372 | ], 373 | }); 374 | 375 | const response = await smartcar.getConnections('fake-amt', { 376 | vehicleId: 'test-vehicle-456', 377 | }); 378 | t.is(response.connections.length, 1); 379 | t.is(response.connections[0].vehicleId, 'test-vehicle-456'); 380 | t.true(n.isDone()); 381 | }); 382 | 383 | test('getConnections - with limit parameter', async function(t) { 384 | const n = nock('https://management.smartcar.com/v2.0/') 385 | .get('/management/connections') 386 | .query({limit: 10}) 387 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 388 | .reply(200, { 389 | connections: [ 390 | {vehicleId: 'vehicle1', userId: 'user1', connectedAt: '2023-01-01'}, 391 | ], 392 | paging: {cursor: 'next-page-cursor'}, 393 | }); 394 | 395 | const response = await smartcar.getConnections('fake-amt', {}, {limit: 10}); 396 | t.is(response.connections.length, 1); 397 | t.is(response.paging.cursor, 'next-page-cursor'); 398 | t.true(n.isDone()); 399 | }); 400 | 401 | test('getConnections - with cursor parameter', async function(t) { 402 | const n = nock('https://management.smartcar.com/v2.0/') 403 | .get('/management/connections') 404 | .query({cursor: 'page-cursor-123'}) 405 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 406 | .reply(200, { 407 | connections: [ 408 | {vehicleId: 'vehicle2', userId: 'user2', connectedAt: '2023-01-02'}, 409 | ], 410 | }); 411 | 412 | const response = await smartcar.getConnections('fake-amt', {}, { 413 | cursor: 'page-cursor-123', 414 | }); 415 | t.is(response.connections.length, 1); 416 | t.is(response.connections[0].vehicleId, 'vehicle2'); 417 | t.true(n.isDone()); 418 | }); 419 | 420 | test('getConnections - with all parameters', async function(t) { 421 | const n = nock('https://management.smartcar.com/v2.0/') 422 | .get('/management/connections') 423 | .query({ 424 | // eslint-disable-next-line camelcase 425 | user_id: 'test-user-789', 426 | limit: 5, 427 | cursor: 'test-cursor', 428 | }) 429 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 430 | .reply(200, { 431 | connections: [ 432 | { 433 | vehicleId: 'vehicle1', 434 | userId: 'test-user-789', 435 | connectedAt: '2023-01-01', 436 | }, 437 | ], 438 | }); 439 | 440 | const response = await smartcar.getConnections( 441 | 'fake-amt', 442 | {userId: 'test-user-789'}, 443 | {limit: 5, cursor: 'test-cursor'}, 444 | ); 445 | t.is(response.connections.length, 1); 446 | t.is(response.connections[0].userId, 'test-user-789'); 447 | t.true(n.isDone()); 448 | }); 449 | 450 | test('deleteConnections - both vehicleId and userId passed', async function(t) { 451 | const error = await t.throwsAsync( 452 | smartcar.deleteConnections('fake-amt', { 453 | vehicleId: 'vehicle id', 454 | userId: 'user id', 455 | }), 456 | ); 457 | t.is( 458 | error.message, 459 | 'Filter can contain EITHER user_id OR vehicle_id, not both', 460 | ); 461 | }); 462 | 463 | test('deleteConnections - with userId filter', async function(t) { 464 | const n = nock('https://management.smartcar.com/v2.0/') 465 | .delete('/management/connections') 466 | // eslint-disable-next-line camelcase 467 | .query({user_id: 'test-user-123'}) 468 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 469 | .reply(200, { 470 | connections: [ 471 | {vehicleId: 'vehicle1', userId: 'test-user-123'}, 472 | {vehicleId: 'vehicle2', userId: 'test-user-123'}, 473 | ], 474 | }); 475 | 476 | const response = await smartcar.deleteConnections('fake-amt', { 477 | userId: 'test-user-123', 478 | }); 479 | t.is(response.connections.length, 2); 480 | t.is(response.connections[0].userId, 'test-user-123'); 481 | t.true(n.isDone()); 482 | }); 483 | 484 | test('deleteConnections - with vehicleId filter', async function(t) { 485 | const n = nock('https://management.smartcar.com/v2.0/') 486 | .delete('/management/connections') 487 | // eslint-disable-next-line camelcase 488 | .query({vehicle_id: 'test-vehicle-456'}) 489 | .matchHeader('Authorization', 'Basic ZGVmYXVsdDpmYWtlLWFtdA==') 490 | .reply(200, { 491 | connections: [ 492 | {vehicleId: 'test-vehicle-456', userId: 'user1'}, 493 | ], 494 | }); 495 | 496 | const response = await smartcar.deleteConnections('fake-amt', { 497 | vehicleId: 'test-vehicle-456', 498 | }); 499 | t.is(response.connections.length, 1); 500 | t.is(response.connections[0].vehicleId, 'test-vehicle-456'); 501 | t.true(n.isDone()); 502 | }); 503 | 504 | test('timeout', async function(t) { 505 | 506 | nock('https://api.smartcar.com/v6.6/') 507 | .get('/custom/endpoint') 508 | .delay(1000) // delay the response to simulate timeout 509 | .reply(200, {success: true}); 510 | 511 | const newService = new SmartcarService({baseUrl: 'https://api.smartcar.com/v6.6', timeout: 1}); 512 | const error = await t.throwsAsync( 513 | newService.request('get', 'custom/endpoint'), 514 | {instanceOf: SmartcarError}, 515 | ); 516 | 517 | t.is(error.message, 'Request timed out'); 518 | t.is(error.statusCode, 408); 519 | }); 520 | 521 | 522 | test('timeout - batch', async function(t) { 523 | nock('https://api.smartcar.com/v6.6/') 524 | .post('/batch', { 525 | requests: [ 526 | {path: '/custom/endpoint'}, 527 | ], 528 | }) 529 | .delay(1000) // delay the response to simulate timeout 530 | .reply(200, {success: true}); 531 | 532 | const newService = new SmartcarService({baseUrl: 'https://api.smartcar.com/v6.6', timeout: 1}); 533 | const error = await t.throwsAsync( 534 | newService.batchRequest(['/custom/endpoint']), 535 | {instanceOf: SmartcarError}, 536 | ); 537 | 538 | t.is(error.message, 'Request timed out'); 539 | t.is(error.statusCode, 408); 540 | }); 541 | 542 | --------------------------------------------------------------------------------