[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node:\n          - 14\n          - 16\n          - 18\n          - 20\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node }}\n      - run: npm install\n      - run: npm test\n      - uses: coverallsapp/github-action@v2\n        if: matrix.node == 20\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\ncoverage/\n"
  },
  {
    "path": ".npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016 Luigi Pinca\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# shopify-token\n\n[![Version npm][npm-shopify-token-badge]][npm-shopify-token]\n[![Build Status][ci-shopify-token-badge]][ci-shopify-token]\n[![Coverage Status][coverage-shopify-token-badge]][coverage-shopify-token]\n\nThis module helps you retrieve an access token for the Shopify REST API. It\nprovides some convenience methods that can be used when implementing the [OAuth\n2.0 flow][shopify-oauth-doc]. No assumptions are made about your server-side\narchitecture, allowing the module to easily adapt to any setup.\n\n## Install\n\n```\nnpm install --save shopify-token\n```\n\n## API\n\nThe module exports a class whose constructor takes an options object.\n\n### `new ShopifyToken(options)`\n\nCreates a new `ShopifyToken` instance.\n\n#### Arguments\n\n- `options` - A plain JavaScript object, e.g. `{ apiKey: 'YOUR_API_KEY' }`.\n\n#### Options\n\n- `apiKey` - Required - A string that specifies the API key of your app.\n- `sharedSecret` - Required - A string that specifies the shared secret of your\n  app.\n- `redirectUri` - Required - A string that specifies the URL where you want to\n  redirect the users after they authorize the app.\n- `scopes` - Optional - An array of strings or a comma-separated string that\n  specifies the list of scopes e.g. `'read_content,read_themes'`. Defaults to\n  `'read_content'`.\n- `timeout` - Optional - A number that specifies the milliseconds to wait for\n  the server to send a response to the HTTPS request initiated by the\n  `getAccessToken` method before aborting it. Defaults to 60000, or 1 minute.\n- `accessMode` - Optional - A string representing the [API access\n  modes][api-access-mode]. Set this option to `'per-user'` to receive an access\n  token that respects the user's permission level when making API requests\n  (called online access). This is strongly recommended for embedded apps.\n  Defaults to offline access mode.\n- `agent` - Optional - An HTTPS agent which will be passed to the HTTPS\n  request made for obtaining the auth token. This is useful when trying to\n  obtain a token from a server that has restrictions on internet access.\n\n#### Return value\n\nA `ShopifyToken` instance.\n\n#### Exceptions\n\nThrows a `Error` exception if the required options are missing.\n\n#### Example\n\n```js\nconst ShopifyToken = require('shopify-token');\n\nconst shopifyToken = new ShopifyToken({\n  sharedSecret: '8ceb18e8ca581aee7cad1ddd3991610b',\n  redirectUri: 'http://localhost:8080/callback',\n  apiKey: 'e74d25b9a6f2b15f2836c954ea8c1711'\n});\n```\n\n### `shopifyToken.generateNonce()`\n\nGenerates a random nonce.\n\n#### Return value\n\nA string representing the nonce.\n\n#### Example\n\n```js\nconst nonce = shopifyToken.generateNonce();\n\nconsole.log(nonce);\n// => 212a8b839860d1aefb258aaffcdbd63f\n```\n\n### `shopifyToken.generateAuthUrl(shop[, scopes[, nonce[, accessMode]]])`\n\nBuilds and returns the authorization URL where you should redirect the user.\n\n#### Arguments\n\n- `shop` - A string that specifies the name of the user's shop.\n- `scopes` - An optional array of strings or comma-separated string to specify\n  the list of scopes. This allows you to override the default scopes.\n- `nonce` - An optional string representing the nonce. If not provided it will\n  be generated automatically.\n- `accessMode` - An optional string dictating the API access mode. If not\n  provided the access mode defined by the `accessMode` constructor option will\n  be used.\n\n#### Return value\n\nA string representing the URL where the user should be redirected.\n\n#### Example\n\n```js\nconst url = shopifyToken.generateAuthUrl('dolciumi');\n\nconsole.log(url);\n// => https://dolciumi.myshopify.com/admin/oauth/authorize?scope=read_content&state=7194ee27dd47ac9efb0ad04e93750e64&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&client_id=e74d25b9a6f2b15f2836c954ea8c1711\n```\n\n### `shopifyToken.verifyHmac(query)`\n\nEvery request or redirect from Shopify to the client server includes a hmac\nparameter that can be used to ensure that it came from Shopify. This method\nvalidates the hmac parameter.\n\n#### Arguments\n\n- `query` - The parsed query string object.\n\n#### Return value\n\n`true` if the hmac is valid, else `false`.\n\n#### Example\n\n```js\nconst ok = shopifyToken.verifyHmac({\n  hmac: 'd1c59b480761bdabf7ee7eb2c09a3d84e71b1d37991bc2872bea8a4c43f8b2b3',\n  signature: '184559898f5bbd1301606e7919c6e67f',\n  state: 'b77827e928ee8eee614b5808d3276c8a',\n  code: '4d732838ad8c22cd1d2dd96f8a403fb7',\n  shop: 'dolciumi.myshopify.com',\n  timestamp: '1452342558'\n});\n\nconsole.log(ok);\n// => true\n```\n\n### `shopifyToken.getAccessToken(hostname, code)`\n\nExchanges the authorization code for a permanent access token.\n\n#### Arguments\n\n- `hostname` - A string that specifies the hostname of the user's shop. e.g.\n  `foo.myshopify.com`. You can get this from the `shop` parameter passed by\n  Shopify in the confirmation redirect.\n- `code` - The authorization Code. You can get this from the `code` parameter\n  passed by Shopify in the confirmation redirect.\n\n#### Return value\n\nA `Promise` which gets resolved with an access token and additional data. When\nthe exchange fails, you can read the HTTPS response status code and body from\nthe `statusCode` and `responseBody` properties which are added to the error\nobject.\n\n#### Example\n\n```js\nconst code = '4d732838ad8c22cd1d2dd96f8a403fb7';\nconst hostname = 'dolciumi.myshopify.com';\n\nshopifyToken\n  .getAccessToken(hostname, code)\n  .then((data) => {\n    console.log(data);\n    // => { access_token: 'f85632530bf277ec9ac6f649fc327f17', scope: 'read_content' }\n  })\n  .catch((err) => console.err(err));\n```\n\n## License\n\n[MIT](LICENSE)\n\n[api-access-mode]: https://shopify.dev/apps/auth/access-modes\n[npm-shopify-token-badge]: https://img.shields.io/npm/v/shopify-token.svg\n[npm-shopify-token]: https://www.npmjs.com/package/shopify-token\n[ci-shopify-token-badge]:\n  https://img.shields.io/github/actions/workflow/status/lpinca/shopify-token/ci.yml?branch=master&label=CI\n[ci-shopify-token]:\n  https://github.com/lpinca/shopify-token/actions?query=workflow%3ACI+branch%3Amaster\n[coverage-shopify-token-badge]:\n  https://img.shields.io/coveralls/lpinca/shopify-token/master.svg\n[coverage-shopify-token]:\n  https://coveralls.io/r/lpinca/shopify-token?branch=master\n[shopify-oauth-doc]: https://shopify.dev/apps/auth/oauth\n"
  },
  {
    "path": "example/README.md",
    "content": "# Example\n\nThis example shows you how you can use `shopify-token` with `express`.\n\nTo run it, edit the `config.json` file and add the client credentials, then\ninstall the dependencies:\n\n```\nnpm install\n```\n\nAfter this you can start the server using `node index.js`. When the server is\nrunning, point your browser to [http://localhost:8080](http://localhost:8080).\n"
  },
  {
    "path": "example/config.json",
    "content": "{\n  \"redirectUri\": \"http://localhost:8080/callback\",\n  \"sharedSecret\": \"APP_SECRET\",\n  \"apiKey\": \"APP_KEY\",\n  \"shop\": \"SHOP_NAME\"\n}\n"
  },
  {
    "path": "example/index.js",
    "content": "'use strict';\n\nconst session = require('express-session');\nconst express = require('express');\n\nconst ShopifyToken = require('..');\nconst config = require('./config');\n\nconst shopifyToken = new ShopifyToken(config);\nconst app = express();\n\napp.use(session({\n  secret: 'eo3Athuo4Ang5gai',\n  saveUninitialized: false,\n  resave: false\n}));\n\napp.get('/', (req, res) => {\n  if (req.session.token) return res.send('Token ready to be used');\n\n  //\n  // Generate a random nonce.\n  //\n  const nonce = shopifyToken.generateNonce();\n\n  //\n  // Generate the authorization URL. For the sake of simplicity the shop name\n  // is fixed here but it can, of course, be passed along with the request and\n  // be different for each request.\n  //\n  const uri = shopifyToken.generateAuthUrl(config.shop, undefined, nonce);\n\n  //\n  // Save the nonce in the session to verify it later.\n  //\n  req.session.state = nonce;\n  res.redirect(uri);\n});\n\napp.get('/callback', (req, res) => {\n  const state = req.query.state;\n\n  if (\n      typeof state !== 'string'\n    || state !== req.session.state          // Validate the state.\n    || !shopifyToken.verifyHmac(req.query)  // Validate the hmac.\n  ) {\n    return res.status(400).send('Security checks failed');\n  }\n\n  //\n  // Exchange the authorization code for a permanent access token.\n  //\n  shopifyToken.getAccessToken(req.query.shop, req.query.code)\n    .then((data) => {\n      const token = data.access_token;\n      console.log(token);\n\n      req.session.token = token;\n      req.session.state = undefined;\n      res.redirect('/');\n    })\n    .catch((err) => {\n      console.error(err.stack);\n      res.status(500).send('Oops, something went wrong');\n    });\n});\n\napp.listen(8080, () => console.log('Open http://localhost:8080 in your browser'));\n"
  },
  {
    "path": "example/package.json",
    "content": "{\n  \"name\": \"shopify-token-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"keywords\": [\n    \"shopify-token\",\n    \"example\"\n  ],\n  \"author\": \"Luigi Pinca\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"express\": \"^4.13.3\",\n    \"express-session\": \"^1.12.1\"\n  }\n}\n"
  },
  {
    "path": "index.js",
    "content": "'use strict';\n\nconst crypto = require('crypto');\nconst https = require('https');\nconst url = require('url');\n\n/**\n * Encode a string by replacing each instance of the `&` and `%` characters\n * with `%26` and `%25` respectively.\n *\n * @param {String} input The input string\n * @return {String} The encoded string\n * @private\n */\nconst encodeValue = (input) => input.replace(/[%&]/g, encodeURIComponent);\n\n/**\n * Encode a string by replacing each instance of the `&`, `%` and `=` characters\n * with `%26`, `%25` and `%3D` respectively.\n *\n * @param {String} input The input string\n * @return {String} The encoded string\n * @private\n */\nconst encodeKey = (input) => input.replace(/[%&=]/g, encodeURIComponent);\n\n/**\n * Check whether two buffers have exactly the same bytes without leaking timing\n * information.\n *\n * @param {Buffer} a One buffer to be tested for equality\n * @param {Buffer} b The other buffer to be tested for equality\n * @return {Boolean} `true` if `a` and `b` have exactly the same bytes, else\n *     `false`\n * @private\n */\nfunction timingSafeEqual(a, b) {\n  let result = 0;\n\n  for (let i = 0; i < a.length; i++) {\n    result |= a[i] ^ b[i];\n  }\n\n  return result === 0;\n}\n\n/**\n * ShopifyToken class.\n */\nclass ShopifyToken {\n  /**\n   * Create a ShopifyToken instance.\n   *\n   * @param {Object} options Configuration options\n   * @param {String} options.redirectUri The redirect URL for the Oauth2 flow\n   * @param {String} options.sharedSecret The Shared Secret for the app\n   * @param {Array|String} [options.scopes] The list of scopes\n   * @param {String} options.apiKey The API Key for the app\n   * @param {String} [options.accessMode] The API access mode\n   * @param {Number} [options.timeout] The request timeout\n   * @param {https.Agent} [options.agent] The agent used for all HTTP requests\n   */\n  constructor(options) {\n    if (\n        !options\n      || !options.sharedSecret\n      || !options.redirectUri\n      || !options.apiKey\n    ) {\n      throw new Error('Missing or invalid options');\n    }\n\n    this.accessMode = 'accessMode' in options ? options.accessMode : '';\n    this.scopes = 'scopes' in options ? options.scopes : 'read_content';\n    this.timeout = 'timeout' in options ? options.timeout : 60000;\n    this.sharedSecret = options.sharedSecret;\n    this.redirectUri = options.redirectUri;\n    this.apiKey = options.apiKey;\n    this.agent = options.agent;\n  }\n\n  /**\n   * Generate a random nonce.\n   *\n   * @return {String} The random nonce\n   * @public\n   */\n  generateNonce() {\n    return crypto.randomBytes(16).toString('hex');\n  }\n\n  /**\n   * Build the authorization URL.\n   *\n   * @param {String} shop The shop name\n   * @param {Array|String} [scopes] The list of scopes\n   * @param {String} [nonce] The nonce\n   * @param {String} [accessMode] The API access mode\n   * @return {String} The authorization URL\n   * @public\n   */\n  generateAuthUrl(shop, scopes, nonce, accessMode) {\n    scopes || (scopes = this.scopes);\n    accessMode || (accessMode = this.accessMode);\n\n    const query = {\n      scope: Array.isArray(scopes) ? scopes.join(',') : scopes,\n      state: nonce || this.generateNonce(),\n      redirect_uri: this.redirectUri,\n      client_id: this.apiKey\n    };\n\n    if (accessMode) {\n      query['grant_options[]'] = accessMode;\n    }\n\n    return url.format({\n      pathname: '/admin/oauth/authorize',\n      hostname: shop.endsWith('.myshopify.com') ? shop : `${shop}.myshopify.com`,\n      protocol: 'https:',\n      query\n    });\n  }\n\n  /**\n   * Verify the hmac returned by Shopify.\n   *\n   * @param {Object} query The parsed query string\n   * @return {Boolean} `true` if the hmac is valid, else `false`\n   * @public\n   */\n  verifyHmac(query) {\n    const pairs = Object.keys(query)\n      .filter((key) => key !== 'signature' && key !== 'hmac')\n      .map((key) => {\n        const value = Array.isArray(query[key])\n          ? `[\"${query[key].join('\", \"')}\"]`\n          : String(query[key]);\n\n        return `${encodeKey(key)}=${encodeValue(value)}`;\n      })\n      .sort();\n\n    if (\n      typeof query.hmac !== 'string' ||\n      Buffer.byteLength(query.hmac) !== 64\n    ) {\n      return false;\n    }\n\n    const digest = crypto.createHmac('sha256', this.sharedSecret)\n      .update(pairs.join('&'))\n      .digest();\n\n    return timingSafeEqual(digest, Buffer.from(query.hmac, 'hex'));\n  }\n\n  /**\n   * Request an access token.\n   *\n   * @param {String} shop The hostname of the shop, e.g. foo.myshopify.com\n   * @param {String} code The authorization code\n   * @return {Promise} Promise which is fulfilled with an access token and\n   *     additional data\n   * @public\n   */\n  getAccessToken(shop, code) {\n    return new Promise((resolve, reject) => {\n      const data = JSON.stringify({\n        client_secret: this.sharedSecret,\n        client_id: this.apiKey,\n        code\n      });\n\n      const request = https.request({\n        headers: {\n          'Content-Length': Buffer.byteLength(data),\n          'Content-Type': 'application/json',\n          'Accept': 'application/json'\n        },\n        path: '/admin/oauth/access_token',\n        hostname: shop,\n        method: 'POST',\n        agent: this.agent\n      });\n\n      let timer = setTimeout(() => {\n        request.abort();\n        timer = null;\n        reject(new Error('Request timed out'));\n      }, this.timeout);\n\n      request.on('response', (response) => {\n        const status = response.statusCode;\n        let body = '';\n\n        response.setEncoding('utf8');\n        response.on('data', (chunk) => body += chunk);\n        response.on('end', () => {\n          let error;\n\n          if (!timer) return;\n\n          clearTimeout(timer);\n\n          if (status !== 200) {\n            error = new Error('Failed to get Shopify access token');\n            error.responseBody = body;\n            error.statusCode = status;\n            return reject(error);\n          }\n\n          try {\n            body = JSON.parse(body);\n          } catch (e) {\n            error = new Error('Failed to parse the response body');\n            error.responseBody = body;\n            error.statusCode = status;\n            return reject(error);\n          }\n\n          resolve(body);\n        });\n      });\n\n      request.on('error', (err) => {\n        if (!timer) return;\n\n        clearTimeout(timer);\n        reject(err);\n      });\n\n      request.end(data);\n    });\n  }\n}\n\nmodule.exports = ShopifyToken;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"shopify-token\",\n  \"version\": \"4.1.0\",\n  \"description\": \"Get an OAuth 2.0 access token for the Shopify API with ease\",\n  \"homepage\": \"https://github.com/lpinca/shopify-token\",\n  \"bugs\": \"https://github.com/lpinca/shopify-token/issues\",\n  \"repository\": \"lpinca/shopify-token\",\n  \"author\": \"Luigi Pinca\",\n  \"license\": \"MIT\",\n  \"main\": \"index.js\",\n  \"engines\": {\n    \"node\": \">=6.6.0\"\n  },\n  \"scripts\": {\n    \"test\": \"c8 --reporter=lcov --reporter=text mocha\"\n  },\n  \"files\": [\n    \"index.js\",\n    \"types/index.d.ts\"\n  ],\n  \"types\": \"types\",\n  \"keywords\": [\n    \"shopify\",\n    \"token\",\n    \"api\",\n    \"oauth\"\n  ],\n  \"devDependencies\": {\n    \"c8\": \"^7.3.0\",\n    \"chai\": \"^4.2.0\",\n    \"mocha\": \"^10.0.0\",\n    \"nock\": \"^13.0.4\",\n    \"pre-commit\": \"^1.2.2\"\n  }\n}\n"
  },
  {
    "path": "test.js",
    "content": "describe('shopify-token', function () {\n  'use strict';\n\n  const expect = require('chai').expect;\n  const stream = require('stream');\n  const https = require('https');\n  const nock = require('nock');\n  const url = require('url');\n\n  const ShopifyToken = require('.');\n\n  const shopifyToken = new ShopifyToken({\n    sharedSecret: 'foo',\n    redirectUri: 'bar',\n    apiKey: 'baz'\n  });\n\n  it('exports the class', function () {\n    expect(ShopifyToken).to.be.a('function');\n  });\n\n  it('throws an error when the required options are missing', function () {\n    expect(() => {\n      new ShopifyToken();\n    }).to.throw(Error, /Missing or invalid options/);\n\n    expect(() => {\n      new ShopifyToken({ scopes: 'write_content' });\n    }).to.throw(Error, /Missing or invalid options/);\n  });\n\n  it('uses a default scope', function () {\n    expect(shopifyToken.scopes).to.equal('read_content');\n  });\n\n  it('defaults to offline access mode', function () {\n    expect(shopifyToken.accessMode).to.equal('');\n  });\n\n  it('allows to customize the default scopes', function () {\n    const shopifyToken = new ShopifyToken({\n      scopes: 'read_content,write_content',\n      sharedSecret: 'foo',\n      redirectUri: 'bar',\n      apiKey: 'baz'\n    });\n\n    expect(shopifyToken.scopes).to.equal('read_content,write_content');\n  });\n\n  it('allows to customize the default access mode', function () {\n    const shopifyToken = new ShopifyToken({\n      accessMode: 'per-user',\n      sharedSecret: 'foo',\n      redirectUri: 'bar',\n      apiKey: 'baz'\n    });\n\n    expect(shopifyToken.accessMode).to.equal('per-user');\n  });\n\n  it('allows to customize the request timeout', function () {\n    const shopifyToken = new ShopifyToken({\n      sharedSecret: 'foo',\n      redirectUri: 'bar',\n      apiKey: 'baz',\n      timeout: 300\n    });\n\n    expect(shopifyToken.timeout).to.equal(300);\n  });\n\n  describe('#generateNonce', function () {\n    it('generates a random nonce', function () {\n      const nonce = shopifyToken.generateNonce();\n\n      expect(nonce).to.be.a('string').and.have.length(32);\n    });\n  });\n\n  describe('#generateAuthUrl', function () {\n    it('builds the authorization URL', function () {\n      const uri = shopifyToken.generateAuthUrl('qux');\n      const nonce = url.parse(uri, true).query.state;\n\n      expect(nonce).to.be.a('string').and.have.length(32);\n      expect(uri).to.equal(url.format({\n        pathname: '/admin/oauth/authorize',\n        hostname: 'qux.myshopify.com',\n        protocol: 'https:',\n        query: {\n          scope: 'read_content',\n          state: nonce,\n          redirect_uri: 'bar',\n          client_id: 'baz'\n        }\n      }));\n    });\n\n    it(\"allows to use the shop's myshopify.com domain as shop name\", function () {\n      const uri = shopifyToken.generateAuthUrl('qux.myshopify.com');\n      const nonce = url.parse(uri, true).query.state;\n\n      expect(nonce).to.be.a('string').and.have.length(32);\n      expect(uri).to.equal(url.format({\n        pathname: '/admin/oauth/authorize',\n        hostname: 'qux.myshopify.com',\n        protocol: 'https:',\n        query: {\n          scope: 'read_content',\n          state: nonce,\n          redirect_uri: 'bar',\n          client_id: 'baz'\n        }\n      }));\n    });\n\n    it('allows to override the default scopes', function () {\n      const uri = shopifyToken.generateAuthUrl('qux', 'read_themes,read_products');\n      const nonce = url.parse(uri, true).query.state;\n\n      expect(nonce).to.be.a('string').and.have.length(32);\n      expect(uri).to.equal(url.format({\n        pathname: '/admin/oauth/authorize',\n        hostname: 'qux.myshopify.com',\n        protocol: 'https:',\n        query: {\n          scope: 'read_themes,read_products',\n          state: nonce,\n          redirect_uri: 'bar',\n          client_id: 'baz'\n        }\n      }));\n    });\n\n    it('allows to use an array to override the scopes', function () {\n      const uri = shopifyToken.generateAuthUrl('qux', [\n        'read_products',\n        'read_themes'\n      ]);\n      const nonce = url.parse(uri, true).query.state;\n\n      expect(nonce).to.be.a('string').and.have.length(32);\n      expect(uri).to.equal(url.format({\n        pathname: '/admin/oauth/authorize',\n        hostname: 'qux.myshopify.com',\n        protocol: 'https:',\n        query: {\n          scope: 'read_products,read_themes',\n          state: nonce,\n          redirect_uri: 'bar',\n          client_id: 'baz'\n        }\n      }));\n    });\n\n    it('allows to use a custom nonce', function () {\n      const uri = shopifyToken.generateAuthUrl('qux', undefined, 'corge');\n\n      expect(uri).to.equal(url.format({\n        pathname: '/admin/oauth/authorize',\n        hostname: 'qux.myshopify.com',\n        protocol: 'https:',\n        query: {\n          scope: 'read_content',\n          state: 'corge',\n          redirect_uri: 'bar',\n          client_id: 'baz'\n        }\n      }));\n    });\n\n    it('allows to override the default access mode', function () {\n      const uri = shopifyToken.generateAuthUrl(\n        'qux',\n        undefined,\n        undefined,\n        'per-user'\n      );\n      const nonce = url.parse(uri, true).query.state;\n\n      expect(nonce).to.be.a('string').and.have.length(32);\n      expect(uri).to.equal(url.format({\n        pathname: '/admin/oauth/authorize',\n        hostname: 'qux.myshopify.com',\n        protocol: 'https:',\n        query: {\n          scope: 'read_content',\n          state: nonce,\n          redirect_uri: 'bar',\n          client_id: 'baz',\n          'grant_options[]': 'per-user'\n        }\n      }));\n    });\n  });\n\n  describe('#verifyHmac', function () {\n    it('returns true if the message is authentic', function () {\n      expect(shopifyToken.verifyHmac({\n        hmac: '3d9b9a7918ac20dfd03b6a0af54a58f0a47980145ae81a37f41597a1e34b528d',\n        state: 'b77827e928ee8eee614b5808d3276c8a',\n        code: '4d732838ad8c22cd1d2dd96f8a403fb7',\n        shop: 'qux.myshopify.com',\n        timestamp: '1451929074'\n      })).to.equal(true);\n\n      expect(shopifyToken.verifyHmac({\n        hmac: 'ffe89c5d47dd26297d47b68e6ad14cf4ee6f11a72b3da7c7a0974d0c3959579a',\n        shop: 'qux.myshopify.com',\n        timestamp: '1492784493',\n        quuz: [1, 2],\n        corge: 'grault'\n      })).to.equal(true);\n    });\n\n    it('returns false if the message is not authentic', function () {\n      expect(shopifyToken.verifyHmac({\n        hmac: '3d9b9a7918ac20dfd03b6a0af54a58f0a47980145ae81a37f41597a1e34b528d',\n        state: 'b77827e928ee8eee614b5808d3276c8a',\n        code: '4d732838ad8c22cd1d2dd96f8a403fb7',\n        shop: 'qux.myshopify.com',\n        timestamp: '1451933938'\n      })).to.equal(false);\n    });\n\n    it('returns false if the query object is empty', function () {\n      expect(shopifyToken.verifyHmac({})).to.equal(false);\n    });\n  });\n\n  describe('#getAccessToken', function () {\n    const pathname = '/admin/oauth/access_token';\n    const hostname = 'qux.myshopify.com';\n    const scope = nock(`https://${hostname}`, { allowUnmocked: true });\n\n    afterEach(function () {\n      expect(scope.isDone()).to.be.true;\n    });\n\n    it('exchanges the auth code for the access token', function () {\n      const code = '4d732838ad8c22cd1d2dd96f8a403fb7';\n      const reply =  {\n        access_token: 'f85632530bf277ec9ac6f649fc327f17',\n        scope: 'read_content'\n      };\n\n      scope\n        .post(pathname, { client_secret: 'foo', client_id: 'baz', code })\n        .reply(200, reply);\n\n      return shopifyToken.getAccessToken(hostname, code)\n        .then((data) => expect(data).to.deep.equal(reply));\n    });\n\n    it('honors the `agent` option', function () {\n      const code = '4d732838ad8c22cd1d2dd96f8a403fb7';\n      const requestBody = {\n        client_secret: 'foo',\n        client_id: 'baz',\n        code\n      };\n      const stringifiedRequestBody = JSON.stringify(requestBody);\n\n      const responseBody =  {\n        access_token: 'f85632530bf277ec9ac6f649fc327f17',\n        scope: 'read_content'\n      };\n      const stringifiedResponseBody = JSON.stringify(responseBody);\n\n      const agent = new https.Agent();\n\n      agent.createConnection = function () {\n        const duplex = new stream.Duplex({\n          read() {},\n          write(chunk, encoding, callback) {\n            if (chunk.length === 0) {\n              callback();\n              return;\n            }\n\n            expect(chunk.toString()).to.equal([\n              `POST ${pathname} HTTP/1.1`,\n              `Content-Length: ${Buffer.byteLength(stringifiedRequestBody)}`,\n              'Content-Type: application/json',\n              'Accept: application/json',\n              `Host: ${hostname}`,\n              'Connection: close',\n              '',\n              stringifiedRequestBody\n            ].join('\\r\\n'));\n\n            duplex.push([\n              'HTTP/1.1 200 OK',\n              'Content-Type: application/json',\n              `Content-Length: ${Buffer.byteLength(stringifiedResponseBody)}`,\n              'Connection: close',\n              `Date: ${new Date().toUTCString()}`,\n              '',\n              stringifiedResponseBody\n            ].join('\\r\\n'));\n\n            callback();\n          }\n        });\n\n        return duplex;\n      }\n\n      const shopifyToken = new ShopifyToken({\n        sharedSecret: 'foo',\n        redirectUri: 'bar',\n        apiKey: 'baz',\n        agent\n      });\n\n      return shopifyToken.getAccessToken(hostname, code)\n        .then((data) => expect(data).to.deep.equal(responseBody));\n    });\n\n    it('returns an error if the request fails', function () {\n      const message = 'Something wrong happened';\n\n      scope\n        .post(pathname)\n        .replyWithError(message);\n\n      return shopifyToken.getAccessToken(hostname, '123456').then(() => {\n        throw new Error('Test invalidation');\n      }, (err) => {\n        expect(err).to.be.an.instanceof(Error);\n        expect(err.message).to.equal(message);\n      });\n    });\n\n    it('returns an error when timeout expires (headers)', function () {\n      const shopifyToken = new ShopifyToken({\n        sharedSecret: 'foo',\n        redirectUri: 'bar',\n        apiKey: 'baz',\n        timeout: 100\n      });\n\n      scope\n        .post(pathname)\n        .delay({ head: 200 })\n        .reply(200, {});\n\n      return shopifyToken.getAccessToken(hostname, '123456').then(() => {\n        throw new Error('Test invalidation');\n      }, (err) => {\n        expect(err).to.be.an.instanceof(Error);\n        expect(err.message).to.equal('Request timed out');\n      });\n    });\n\n    it('returns an error when timeout expires (body)', function () {\n      const shopifyToken = new ShopifyToken({\n        sharedSecret: 'foo',\n        redirectUri: 'bar',\n        apiKey: 'baz',\n        timeout: 100\n      });\n\n      scope\n        .post(pathname)\n        .delay({ body: 200 })\n        .reply(200, {});\n\n      return shopifyToken.getAccessToken(hostname, '123456').then(() => {\n        throw new Error('Test invalidation');\n      }, (err) => {\n        expect(err).to.be.an.instanceof(Error);\n        expect(err.message).to.equal('Request timed out');\n      });\n    });\n\n    it('returns an error when timeout expires (connection)', function () {\n      const shopifyToken = new ShopifyToken({\n        sharedSecret: 'foo',\n        redirectUri: 'bar',\n        apiKey: 'baz',\n        timeout: 100\n      });\n\n      //\n      // `scope.delay()` can only delay the `response` event. The connection is\n      // still established so it is useless for this test. To work around this\n      // issue a non-routable IP address is used here instead of `nock`. See\n      // https://tools.ietf.org/html/rfc5737#section-3\n      //\n      return shopifyToken.getAccessToken('192.0.2.1', '123456').then(() => {\n        throw new Error('Test invalidation');\n      }, (err) => {\n        expect(err).to.be.an.instanceof(Error);\n        expect(err.message).to.equal('Request timed out');\n      });\n    });\n\n    it('returns an error if response statusCode is not 200', function () {\n      const body = 'some error message from shopify';\n\n      scope\n        .post(pathname)\n        .reply(400, body);\n\n      return shopifyToken.getAccessToken(hostname, '123456').then(() => {\n        throw new Error('Test invalidation');\n      }, (err) => {\n        expect(err).to.be.an.instanceof(Error);\n        expect(err).to.have.property('message', 'Failed to get Shopify access token');\n        expect(err).to.have.property('responseBody', body);\n        expect(err).to.have.property('statusCode', 400);\n      });\n    });\n\n    it('returns an error if JSON.parse throws', function () {\n      const body = '<!DOCTYPE html><html><head></head><body></body></html>';\n\n      scope\n        .post(pathname)\n        .reply(200, body);\n\n      return shopifyToken.getAccessToken(hostname, '123456').then(() => {\n        throw new Error('Test invalidation');\n      }, (err) => {\n        expect(err).to.be.an.instanceof(Error);\n        expect(err).to.have.property('message', 'Failed to parse the response body');\n        expect(err).to.have.property('responseBody', body);\n        expect(err).to.have.property('statusCode', 200);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "types/index.d.ts",
    "content": "/// <reference types=\"node\" />\nimport { Agent } from 'https';\n\ndeclare namespace ShopifyToken {\n  export interface ShopifyTokenOptions {\n    // The redirect URL for the Oauth2 flow\n    redirectUri: string;\n    // The Shared Secret for the app\n    sharedSecret: string;\n    // The API Key for the app\n    apiKey: string;\n    // The list of scopes\n    scopes?: string | string[];\n    // The request timeout\n    timeout?: number;\n    // API access mode\n    accessMode?: string;\n    // The agent used for all HTTP requests\n    agent?: Agent;\n  }\n\n  export interface OfflineAccessTokenData {\n    access_token: string;\n    scope: string;\n  }\n\n  export interface AccessTokenAssociatedUser {\n    id: number;\n    first_name: string;\n    last_name: string;\n    email: string;\n    email_verified: boolean;\n    account_owner: boolean;\n    locale: string;\n    collaborator: boolean;\n  }\n\n  export interface OnlineAccessTokenData {\n    access_token: string;\n    scope: string;\n    expires_in: number;\n    associated_user_scope: string;\n    associated_user: AccessTokenAssociatedUser;\n  }\n}\n\ndeclare class ShopifyToken {\n  /**\n   * Create a ShopifyToken instance.\n   *\n   * @param {Object} options Configuration options\n   * @param {String} options.redirectUri The redirect URL for the Oauth2 flow\n   * @param {String} options.sharedSecret The Shared Secret for the app\n   * @param {Array|String} [options.scopes] The list of scopes\n   * @param {String} options.apiKey The API Key for the app\n   * @param {String} [options.accessMode] The API access mode\n   * @param {Number} [options.timeout] The request timeout\n   * @param {Agent} [options.agent] The agent used for all HTTP requests\n   */\n  constructor(options: ShopifyToken.ShopifyTokenOptions);\n  /**\n   * Generate a random nonce.\n   *\n   * @return {String} The random nonce\n   * @public\n   */\n  generateNonce(): string;\n  /**\n   * Build the authorization URL.\n   *\n   * @param {String} shop The shop name\n   * @param {Array|String} [scopes] The list of scopes\n   * @param {String} [nonce] The nonce\n   * @param {String} [accessMode] The API access mode\n   * @return {String} The authorization URL\n   * @public\n   */\n  generateAuthUrl(\n    shop: string,\n    scopes?: string | string[],\n    nonce?: string,\n    accessMode?: string\n  ): string;\n  /**\n   * Verify the hmac returned by Shopify.\n   *\n   * @param {Object} query The parsed query string\n   * @return {Boolean} `true` if the hmac is valid, else `false`\n   * @public\n   */\n  verifyHmac(query: any): boolean;\n  /**\n   * Request an access token.\n   *\n   * @param {String} shop The hostname of the shop, e.g. foo.myshopify.com\n   * @param {String} code The authorization code\n   * @return {Promise} Promise which is fulfilled with an access token and\n   *     additional data\n   * @public\n   */\n  getAccessToken(\n    shop: string,\n    code: string\n  ): Promise<\n    ShopifyToken.OfflineAccessTokenData | ShopifyToken.OnlineAccessTokenData\n  >;\n}\n\nexport = ShopifyToken;\n"
  }
]