Full Code of lpinca/shopify-token for AI

master 23b559e5ce42 cached
13 files
32.9 KB
8.9k tokens
14 symbols
1 requests
Download .txt
Repository: lpinca/shopify-token
Branch: master
Commit: 23b559e5ce42
Files: 13
Total size: 32.9 KB

Directory structure:
gitextract_fvi5qwv6/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── example/
│   ├── README.md
│   ├── config.json
│   ├── index.js
│   └── package.json
├── index.js
├── package.json
├── test.js
└── types/
    └── index.d.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  - push
  - pull_request

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node:
          - 14
          - 16
          - 18
          - 20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm install
      - run: npm test
      - uses: coverallsapp/github-action@v2
        if: matrix.node == 20
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
node_modules/
coverage/


================================================
FILE: .npmrc
================================================
package-lock=false


================================================
FILE: LICENSE
================================================
Copyright (c) 2016 Luigi Pinca

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# shopify-token

[![Version npm][npm-shopify-token-badge]][npm-shopify-token]
[![Build Status][ci-shopify-token-badge]][ci-shopify-token]
[![Coverage Status][coverage-shopify-token-badge]][coverage-shopify-token]

This module helps you retrieve an access token for the Shopify REST API. It
provides some convenience methods that can be used when implementing the [OAuth
2.0 flow][shopify-oauth-doc]. No assumptions are made about your server-side
architecture, allowing the module to easily adapt to any setup.

## Install

```
npm install --save shopify-token
```

## API

The module exports a class whose constructor takes an options object.

### `new ShopifyToken(options)`

Creates a new `ShopifyToken` instance.

#### Arguments

- `options` - A plain JavaScript object, e.g. `{ apiKey: 'YOUR_API_KEY' }`.

#### Options

- `apiKey` - Required - A string that specifies the API key of your app.
- `sharedSecret` - Required - A string that specifies the shared secret of your
  app.
- `redirectUri` - Required - A string that specifies the URL where you want to
  redirect the users after they authorize the app.
- `scopes` - Optional - An array of strings or a comma-separated string that
  specifies the list of scopes e.g. `'read_content,read_themes'`. Defaults to
  `'read_content'`.
- `timeout` - Optional - A number that specifies the milliseconds to wait for
  the server to send a response to the HTTPS request initiated by the
  `getAccessToken` method before aborting it. Defaults to 60000, or 1 minute.
- `accessMode` - Optional - A string representing the [API access
  modes][api-access-mode]. Set this option to `'per-user'` to receive an access
  token that respects the user's permission level when making API requests
  (called online access). This is strongly recommended for embedded apps.
  Defaults to offline access mode.
- `agent` - Optional - An HTTPS agent which will be passed to the HTTPS
  request made for obtaining the auth token. This is useful when trying to
  obtain a token from a server that has restrictions on internet access.

#### Return value

A `ShopifyToken` instance.

#### Exceptions

Throws a `Error` exception if the required options are missing.

#### Example

```js
const ShopifyToken = require('shopify-token');

const shopifyToken = new ShopifyToken({
  sharedSecret: '8ceb18e8ca581aee7cad1ddd3991610b',
  redirectUri: 'http://localhost:8080/callback',
  apiKey: 'e74d25b9a6f2b15f2836c954ea8c1711'
});
```

### `shopifyToken.generateNonce()`

Generates a random nonce.

#### Return value

A string representing the nonce.

#### Example

```js
const nonce = shopifyToken.generateNonce();

console.log(nonce);
// => 212a8b839860d1aefb258aaffcdbd63f
```

### `shopifyToken.generateAuthUrl(shop[, scopes[, nonce[, accessMode]]])`

Builds and returns the authorization URL where you should redirect the user.

#### Arguments

- `shop` - A string that specifies the name of the user's shop.
- `scopes` - An optional array of strings or comma-separated string to specify
  the list of scopes. This allows you to override the default scopes.
- `nonce` - An optional string representing the nonce. If not provided it will
  be generated automatically.
- `accessMode` - An optional string dictating the API access mode. If not
  provided the access mode defined by the `accessMode` constructor option will
  be used.

#### Return value

A string representing the URL where the user should be redirected.

#### Example

```js
const url = shopifyToken.generateAuthUrl('dolciumi');

console.log(url);
// => https://dolciumi.myshopify.com/admin/oauth/authorize?scope=read_content&state=7194ee27dd47ac9efb0ad04e93750e64&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&client_id=e74d25b9a6f2b15f2836c954ea8c1711
```

### `shopifyToken.verifyHmac(query)`

Every request or redirect from Shopify to the client server includes a hmac
parameter that can be used to ensure that it came from Shopify. This method
validates the hmac parameter.

#### Arguments

- `query` - The parsed query string object.

#### Return value

`true` if the hmac is valid, else `false`.

#### Example

```js
const ok = shopifyToken.verifyHmac({
  hmac: 'd1c59b480761bdabf7ee7eb2c09a3d84e71b1d37991bc2872bea8a4c43f8b2b3',
  signature: '184559898f5bbd1301606e7919c6e67f',
  state: 'b77827e928ee8eee614b5808d3276c8a',
  code: '4d732838ad8c22cd1d2dd96f8a403fb7',
  shop: 'dolciumi.myshopify.com',
  timestamp: '1452342558'
});

console.log(ok);
// => true
```

### `shopifyToken.getAccessToken(hostname, code)`

Exchanges the authorization code for a permanent access token.

#### Arguments

- `hostname` - A string that specifies the hostname of the user's shop. e.g.
  `foo.myshopify.com`. You can get this from the `shop` parameter passed by
  Shopify in the confirmation redirect.
- `code` - The authorization Code. You can get this from the `code` parameter
  passed by Shopify in the confirmation redirect.

#### Return value

A `Promise` which gets resolved with an access token and additional data. When
the exchange fails, you can read the HTTPS response status code and body from
the `statusCode` and `responseBody` properties which are added to the error
object.

#### Example

```js
const code = '4d732838ad8c22cd1d2dd96f8a403fb7';
const hostname = 'dolciumi.myshopify.com';

shopifyToken
  .getAccessToken(hostname, code)
  .then((data) => {
    console.log(data);
    // => { access_token: 'f85632530bf277ec9ac6f649fc327f17', scope: 'read_content' }
  })
  .catch((err) => console.err(err));
```

## License

[MIT](LICENSE)

[api-access-mode]: https://shopify.dev/apps/auth/access-modes
[npm-shopify-token-badge]: https://img.shields.io/npm/v/shopify-token.svg
[npm-shopify-token]: https://www.npmjs.com/package/shopify-token
[ci-shopify-token-badge]:
  https://img.shields.io/github/actions/workflow/status/lpinca/shopify-token/ci.yml?branch=master&label=CI
[ci-shopify-token]:
  https://github.com/lpinca/shopify-token/actions?query=workflow%3ACI+branch%3Amaster
[coverage-shopify-token-badge]:
  https://img.shields.io/coveralls/lpinca/shopify-token/master.svg
[coverage-shopify-token]:
  https://coveralls.io/r/lpinca/shopify-token?branch=master
[shopify-oauth-doc]: https://shopify.dev/apps/auth/oauth


================================================
FILE: example/README.md
================================================
# Example

This example shows you how you can use `shopify-token` with `express`.

To run it, edit the `config.json` file and add the client credentials, then
install the dependencies:

```
npm install
```

After this you can start the server using `node index.js`. When the server is
running, point your browser to [http://localhost:8080](http://localhost:8080).


================================================
FILE: example/config.json
================================================
{
  "redirectUri": "http://localhost:8080/callback",
  "sharedSecret": "APP_SECRET",
  "apiKey": "APP_KEY",
  "shop": "SHOP_NAME"
}


================================================
FILE: example/index.js
================================================
'use strict';

const session = require('express-session');
const express = require('express');

const ShopifyToken = require('..');
const config = require('./config');

const shopifyToken = new ShopifyToken(config);
const app = express();

app.use(session({
  secret: 'eo3Athuo4Ang5gai',
  saveUninitialized: false,
  resave: false
}));

app.get('/', (req, res) => {
  if (req.session.token) return res.send('Token ready to be used');

  //
  // Generate a random nonce.
  //
  const nonce = shopifyToken.generateNonce();

  //
  // Generate the authorization URL. For the sake of simplicity the shop name
  // is fixed here but it can, of course, be passed along with the request and
  // be different for each request.
  //
  const uri = shopifyToken.generateAuthUrl(config.shop, undefined, nonce);

  //
  // Save the nonce in the session to verify it later.
  //
  req.session.state = nonce;
  res.redirect(uri);
});

app.get('/callback', (req, res) => {
  const state = req.query.state;

  if (
      typeof state !== 'string'
    || state !== req.session.state          // Validate the state.
    || !shopifyToken.verifyHmac(req.query)  // Validate the hmac.
  ) {
    return res.status(400).send('Security checks failed');
  }

  //
  // Exchange the authorization code for a permanent access token.
  //
  shopifyToken.getAccessToken(req.query.shop, req.query.code)
    .then((data) => {
      const token = data.access_token;
      console.log(token);

      req.session.token = token;
      req.session.state = undefined;
      res.redirect('/');
    })
    .catch((err) => {
      console.error(err.stack);
      res.status(500).send('Oops, something went wrong');
    });
});

app.listen(8080, () => console.log('Open http://localhost:8080 in your browser'));


================================================
FILE: example/package.json
================================================
{
  "name": "shopify-token-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "keywords": [
    "shopify-token",
    "example"
  ],
  "author": "Luigi Pinca",
  "license": "MIT",
  "dependencies": {
    "express": "^4.13.3",
    "express-session": "^1.12.1"
  }
}


================================================
FILE: index.js
================================================
'use strict';

const crypto = require('crypto');
const https = require('https');
const url = require('url');

/**
 * Encode a string by replacing each instance of the `&` and `%` characters
 * with `%26` and `%25` respectively.
 *
 * @param {String} input The input string
 * @return {String} The encoded string
 * @private
 */
const encodeValue = (input) => input.replace(/[%&]/g, encodeURIComponent);

/**
 * Encode a string by replacing each instance of the `&`, `%` and `=` characters
 * with `%26`, `%25` and `%3D` respectively.
 *
 * @param {String} input The input string
 * @return {String} The encoded string
 * @private
 */
const encodeKey = (input) => input.replace(/[%&=]/g, encodeURIComponent);

/**
 * Check whether two buffers have exactly the same bytes without leaking timing
 * information.
 *
 * @param {Buffer} a One buffer to be tested for equality
 * @param {Buffer} b The other buffer to be tested for equality
 * @return {Boolean} `true` if `a` and `b` have exactly the same bytes, else
 *     `false`
 * @private
 */
function timingSafeEqual(a, b) {
  let result = 0;

  for (let i = 0; i < a.length; i++) {
    result |= a[i] ^ b[i];
  }

  return result === 0;
}

/**
 * ShopifyToken class.
 */
class ShopifyToken {
  /**
   * Create a ShopifyToken instance.
   *
   * @param {Object} options Configuration options
   * @param {String} options.redirectUri The redirect URL for the Oauth2 flow
   * @param {String} options.sharedSecret The Shared Secret for the app
   * @param {Array|String} [options.scopes] The list of scopes
   * @param {String} options.apiKey The API Key for the app
   * @param {String} [options.accessMode] The API access mode
   * @param {Number} [options.timeout] The request timeout
   * @param {https.Agent} [options.agent] The agent used for all HTTP requests
   */
  constructor(options) {
    if (
        !options
      || !options.sharedSecret
      || !options.redirectUri
      || !options.apiKey
    ) {
      throw new Error('Missing or invalid options');
    }

    this.accessMode = 'accessMode' in options ? options.accessMode : '';
    this.scopes = 'scopes' in options ? options.scopes : 'read_content';
    this.timeout = 'timeout' in options ? options.timeout : 60000;
    this.sharedSecret = options.sharedSecret;
    this.redirectUri = options.redirectUri;
    this.apiKey = options.apiKey;
    this.agent = options.agent;
  }

  /**
   * Generate a random nonce.
   *
   * @return {String} The random nonce
   * @public
   */
  generateNonce() {
    return crypto.randomBytes(16).toString('hex');
  }

  /**
   * Build the authorization URL.
   *
   * @param {String} shop The shop name
   * @param {Array|String} [scopes] The list of scopes
   * @param {String} [nonce] The nonce
   * @param {String} [accessMode] The API access mode
   * @return {String} The authorization URL
   * @public
   */
  generateAuthUrl(shop, scopes, nonce, accessMode) {
    scopes || (scopes = this.scopes);
    accessMode || (accessMode = this.accessMode);

    const query = {
      scope: Array.isArray(scopes) ? scopes.join(',') : scopes,
      state: nonce || this.generateNonce(),
      redirect_uri: this.redirectUri,
      client_id: this.apiKey
    };

    if (accessMode) {
      query['grant_options[]'] = accessMode;
    }

    return url.format({
      pathname: '/admin/oauth/authorize',
      hostname: shop.endsWith('.myshopify.com') ? shop : `${shop}.myshopify.com`,
      protocol: 'https:',
      query
    });
  }

  /**
   * Verify the hmac returned by Shopify.
   *
   * @param {Object} query The parsed query string
   * @return {Boolean} `true` if the hmac is valid, else `false`
   * @public
   */
  verifyHmac(query) {
    const pairs = Object.keys(query)
      .filter((key) => key !== 'signature' && key !== 'hmac')
      .map((key) => {
        const value = Array.isArray(query[key])
          ? `["${query[key].join('", "')}"]`
          : String(query[key]);

        return `${encodeKey(key)}=${encodeValue(value)}`;
      })
      .sort();

    if (
      typeof query.hmac !== 'string' ||
      Buffer.byteLength(query.hmac) !== 64
    ) {
      return false;
    }

    const digest = crypto.createHmac('sha256', this.sharedSecret)
      .update(pairs.join('&'))
      .digest();

    return timingSafeEqual(digest, Buffer.from(query.hmac, 'hex'));
  }

  /**
   * Request an access token.
   *
   * @param {String} shop The hostname of the shop, e.g. foo.myshopify.com
   * @param {String} code The authorization code
   * @return {Promise} Promise which is fulfilled with an access token and
   *     additional data
   * @public
   */
  getAccessToken(shop, code) {
    return new Promise((resolve, reject) => {
      const data = JSON.stringify({
        client_secret: this.sharedSecret,
        client_id: this.apiKey,
        code
      });

      const request = https.request({
        headers: {
          'Content-Length': Buffer.byteLength(data),
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        },
        path: '/admin/oauth/access_token',
        hostname: shop,
        method: 'POST',
        agent: this.agent
      });

      let timer = setTimeout(() => {
        request.abort();
        timer = null;
        reject(new Error('Request timed out'));
      }, this.timeout);

      request.on('response', (response) => {
        const status = response.statusCode;
        let body = '';

        response.setEncoding('utf8');
        response.on('data', (chunk) => body += chunk);
        response.on('end', () => {
          let error;

          if (!timer) return;

          clearTimeout(timer);

          if (status !== 200) {
            error = new Error('Failed to get Shopify access token');
            error.responseBody = body;
            error.statusCode = status;
            return reject(error);
          }

          try {
            body = JSON.parse(body);
          } catch (e) {
            error = new Error('Failed to parse the response body');
            error.responseBody = body;
            error.statusCode = status;
            return reject(error);
          }

          resolve(body);
        });
      });

      request.on('error', (err) => {
        if (!timer) return;

        clearTimeout(timer);
        reject(err);
      });

      request.end(data);
    });
  }
}

module.exports = ShopifyToken;


================================================
FILE: package.json
================================================
{
  "name": "shopify-token",
  "version": "4.1.0",
  "description": "Get an OAuth 2.0 access token for the Shopify API with ease",
  "homepage": "https://github.com/lpinca/shopify-token",
  "bugs": "https://github.com/lpinca/shopify-token/issues",
  "repository": "lpinca/shopify-token",
  "author": "Luigi Pinca",
  "license": "MIT",
  "main": "index.js",
  "engines": {
    "node": ">=6.6.0"
  },
  "scripts": {
    "test": "c8 --reporter=lcov --reporter=text mocha"
  },
  "files": [
    "index.js",
    "types/index.d.ts"
  ],
  "types": "types",
  "keywords": [
    "shopify",
    "token",
    "api",
    "oauth"
  ],
  "devDependencies": {
    "c8": "^7.3.0",
    "chai": "^4.2.0",
    "mocha": "^10.0.0",
    "nock": "^13.0.4",
    "pre-commit": "^1.2.2"
  }
}


================================================
FILE: test.js
================================================
describe('shopify-token', function () {
  'use strict';

  const expect = require('chai').expect;
  const stream = require('stream');
  const https = require('https');
  const nock = require('nock');
  const url = require('url');

  const ShopifyToken = require('.');

  const shopifyToken = new ShopifyToken({
    sharedSecret: 'foo',
    redirectUri: 'bar',
    apiKey: 'baz'
  });

  it('exports the class', function () {
    expect(ShopifyToken).to.be.a('function');
  });

  it('throws an error when the required options are missing', function () {
    expect(() => {
      new ShopifyToken();
    }).to.throw(Error, /Missing or invalid options/);

    expect(() => {
      new ShopifyToken({ scopes: 'write_content' });
    }).to.throw(Error, /Missing or invalid options/);
  });

  it('uses a default scope', function () {
    expect(shopifyToken.scopes).to.equal('read_content');
  });

  it('defaults to offline access mode', function () {
    expect(shopifyToken.accessMode).to.equal('');
  });

  it('allows to customize the default scopes', function () {
    const shopifyToken = new ShopifyToken({
      scopes: 'read_content,write_content',
      sharedSecret: 'foo',
      redirectUri: 'bar',
      apiKey: 'baz'
    });

    expect(shopifyToken.scopes).to.equal('read_content,write_content');
  });

  it('allows to customize the default access mode', function () {
    const shopifyToken = new ShopifyToken({
      accessMode: 'per-user',
      sharedSecret: 'foo',
      redirectUri: 'bar',
      apiKey: 'baz'
    });

    expect(shopifyToken.accessMode).to.equal('per-user');
  });

  it('allows to customize the request timeout', function () {
    const shopifyToken = new ShopifyToken({
      sharedSecret: 'foo',
      redirectUri: 'bar',
      apiKey: 'baz',
      timeout: 300
    });

    expect(shopifyToken.timeout).to.equal(300);
  });

  describe('#generateNonce', function () {
    it('generates a random nonce', function () {
      const nonce = shopifyToken.generateNonce();

      expect(nonce).to.be.a('string').and.have.length(32);
    });
  });

  describe('#generateAuthUrl', function () {
    it('builds the authorization URL', function () {
      const uri = shopifyToken.generateAuthUrl('qux');
      const nonce = url.parse(uri, true).query.state;

      expect(nonce).to.be.a('string').and.have.length(32);
      expect(uri).to.equal(url.format({
        pathname: '/admin/oauth/authorize',
        hostname: 'qux.myshopify.com',
        protocol: 'https:',
        query: {
          scope: 'read_content',
          state: nonce,
          redirect_uri: 'bar',
          client_id: 'baz'
        }
      }));
    });

    it("allows to use the shop's myshopify.com domain as shop name", function () {
      const uri = shopifyToken.generateAuthUrl('qux.myshopify.com');
      const nonce = url.parse(uri, true).query.state;

      expect(nonce).to.be.a('string').and.have.length(32);
      expect(uri).to.equal(url.format({
        pathname: '/admin/oauth/authorize',
        hostname: 'qux.myshopify.com',
        protocol: 'https:',
        query: {
          scope: 'read_content',
          state: nonce,
          redirect_uri: 'bar',
          client_id: 'baz'
        }
      }));
    });

    it('allows to override the default scopes', function () {
      const uri = shopifyToken.generateAuthUrl('qux', 'read_themes,read_products');
      const nonce = url.parse(uri, true).query.state;

      expect(nonce).to.be.a('string').and.have.length(32);
      expect(uri).to.equal(url.format({
        pathname: '/admin/oauth/authorize',
        hostname: 'qux.myshopify.com',
        protocol: 'https:',
        query: {
          scope: 'read_themes,read_products',
          state: nonce,
          redirect_uri: 'bar',
          client_id: 'baz'
        }
      }));
    });

    it('allows to use an array to override the scopes', function () {
      const uri = shopifyToken.generateAuthUrl('qux', [
        'read_products',
        'read_themes'
      ]);
      const nonce = url.parse(uri, true).query.state;

      expect(nonce).to.be.a('string').and.have.length(32);
      expect(uri).to.equal(url.format({
        pathname: '/admin/oauth/authorize',
        hostname: 'qux.myshopify.com',
        protocol: 'https:',
        query: {
          scope: 'read_products,read_themes',
          state: nonce,
          redirect_uri: 'bar',
          client_id: 'baz'
        }
      }));
    });

    it('allows to use a custom nonce', function () {
      const uri = shopifyToken.generateAuthUrl('qux', undefined, 'corge');

      expect(uri).to.equal(url.format({
        pathname: '/admin/oauth/authorize',
        hostname: 'qux.myshopify.com',
        protocol: 'https:',
        query: {
          scope: 'read_content',
          state: 'corge',
          redirect_uri: 'bar',
          client_id: 'baz'
        }
      }));
    });

    it('allows to override the default access mode', function () {
      const uri = shopifyToken.generateAuthUrl(
        'qux',
        undefined,
        undefined,
        'per-user'
      );
      const nonce = url.parse(uri, true).query.state;

      expect(nonce).to.be.a('string').and.have.length(32);
      expect(uri).to.equal(url.format({
        pathname: '/admin/oauth/authorize',
        hostname: 'qux.myshopify.com',
        protocol: 'https:',
        query: {
          scope: 'read_content',
          state: nonce,
          redirect_uri: 'bar',
          client_id: 'baz',
          'grant_options[]': 'per-user'
        }
      }));
    });
  });

  describe('#verifyHmac', function () {
    it('returns true if the message is authentic', function () {
      expect(shopifyToken.verifyHmac({
        hmac: '3d9b9a7918ac20dfd03b6a0af54a58f0a47980145ae81a37f41597a1e34b528d',
        state: 'b77827e928ee8eee614b5808d3276c8a',
        code: '4d732838ad8c22cd1d2dd96f8a403fb7',
        shop: 'qux.myshopify.com',
        timestamp: '1451929074'
      })).to.equal(true);

      expect(shopifyToken.verifyHmac({
        hmac: 'ffe89c5d47dd26297d47b68e6ad14cf4ee6f11a72b3da7c7a0974d0c3959579a',
        shop: 'qux.myshopify.com',
        timestamp: '1492784493',
        quuz: [1, 2],
        corge: 'grault'
      })).to.equal(true);
    });

    it('returns false if the message is not authentic', function () {
      expect(shopifyToken.verifyHmac({
        hmac: '3d9b9a7918ac20dfd03b6a0af54a58f0a47980145ae81a37f41597a1e34b528d',
        state: 'b77827e928ee8eee614b5808d3276c8a',
        code: '4d732838ad8c22cd1d2dd96f8a403fb7',
        shop: 'qux.myshopify.com',
        timestamp: '1451933938'
      })).to.equal(false);
    });

    it('returns false if the query object is empty', function () {
      expect(shopifyToken.verifyHmac({})).to.equal(false);
    });
  });

  describe('#getAccessToken', function () {
    const pathname = '/admin/oauth/access_token';
    const hostname = 'qux.myshopify.com';
    const scope = nock(`https://${hostname}`, { allowUnmocked: true });

    afterEach(function () {
      expect(scope.isDone()).to.be.true;
    });

    it('exchanges the auth code for the access token', function () {
      const code = '4d732838ad8c22cd1d2dd96f8a403fb7';
      const reply =  {
        access_token: 'f85632530bf277ec9ac6f649fc327f17',
        scope: 'read_content'
      };

      scope
        .post(pathname, { client_secret: 'foo', client_id: 'baz', code })
        .reply(200, reply);

      return shopifyToken.getAccessToken(hostname, code)
        .then((data) => expect(data).to.deep.equal(reply));
    });

    it('honors the `agent` option', function () {
      const code = '4d732838ad8c22cd1d2dd96f8a403fb7';
      const requestBody = {
        client_secret: 'foo',
        client_id: 'baz',
        code
      };
      const stringifiedRequestBody = JSON.stringify(requestBody);

      const responseBody =  {
        access_token: 'f85632530bf277ec9ac6f649fc327f17',
        scope: 'read_content'
      };
      const stringifiedResponseBody = JSON.stringify(responseBody);

      const agent = new https.Agent();

      agent.createConnection = function () {
        const duplex = new stream.Duplex({
          read() {},
          write(chunk, encoding, callback) {
            if (chunk.length === 0) {
              callback();
              return;
            }

            expect(chunk.toString()).to.equal([
              `POST ${pathname} HTTP/1.1`,
              `Content-Length: ${Buffer.byteLength(stringifiedRequestBody)}`,
              'Content-Type: application/json',
              'Accept: application/json',
              `Host: ${hostname}`,
              'Connection: close',
              '',
              stringifiedRequestBody
            ].join('\r\n'));

            duplex.push([
              'HTTP/1.1 200 OK',
              'Content-Type: application/json',
              `Content-Length: ${Buffer.byteLength(stringifiedResponseBody)}`,
              'Connection: close',
              `Date: ${new Date().toUTCString()}`,
              '',
              stringifiedResponseBody
            ].join('\r\n'));

            callback();
          }
        });

        return duplex;
      }

      const shopifyToken = new ShopifyToken({
        sharedSecret: 'foo',
        redirectUri: 'bar',
        apiKey: 'baz',
        agent
      });

      return shopifyToken.getAccessToken(hostname, code)
        .then((data) => expect(data).to.deep.equal(responseBody));
    });

    it('returns an error if the request fails', function () {
      const message = 'Something wrong happened';

      scope
        .post(pathname)
        .replyWithError(message);

      return shopifyToken.getAccessToken(hostname, '123456').then(() => {
        throw new Error('Test invalidation');
      }, (err) => {
        expect(err).to.be.an.instanceof(Error);
        expect(err.message).to.equal(message);
      });
    });

    it('returns an error when timeout expires (headers)', function () {
      const shopifyToken = new ShopifyToken({
        sharedSecret: 'foo',
        redirectUri: 'bar',
        apiKey: 'baz',
        timeout: 100
      });

      scope
        .post(pathname)
        .delay({ head: 200 })
        .reply(200, {});

      return shopifyToken.getAccessToken(hostname, '123456').then(() => {
        throw new Error('Test invalidation');
      }, (err) => {
        expect(err).to.be.an.instanceof(Error);
        expect(err.message).to.equal('Request timed out');
      });
    });

    it('returns an error when timeout expires (body)', function () {
      const shopifyToken = new ShopifyToken({
        sharedSecret: 'foo',
        redirectUri: 'bar',
        apiKey: 'baz',
        timeout: 100
      });

      scope
        .post(pathname)
        .delay({ body: 200 })
        .reply(200, {});

      return shopifyToken.getAccessToken(hostname, '123456').then(() => {
        throw new Error('Test invalidation');
      }, (err) => {
        expect(err).to.be.an.instanceof(Error);
        expect(err.message).to.equal('Request timed out');
      });
    });

    it('returns an error when timeout expires (connection)', function () {
      const shopifyToken = new ShopifyToken({
        sharedSecret: 'foo',
        redirectUri: 'bar',
        apiKey: 'baz',
        timeout: 100
      });

      //
      // `scope.delay()` can only delay the `response` event. The connection is
      // still established so it is useless for this test. To work around this
      // issue a non-routable IP address is used here instead of `nock`. See
      // https://tools.ietf.org/html/rfc5737#section-3
      //
      return shopifyToken.getAccessToken('192.0.2.1', '123456').then(() => {
        throw new Error('Test invalidation');
      }, (err) => {
        expect(err).to.be.an.instanceof(Error);
        expect(err.message).to.equal('Request timed out');
      });
    });

    it('returns an error if response statusCode is not 200', function () {
      const body = 'some error message from shopify';

      scope
        .post(pathname)
        .reply(400, body);

      return shopifyToken.getAccessToken(hostname, '123456').then(() => {
        throw new Error('Test invalidation');
      }, (err) => {
        expect(err).to.be.an.instanceof(Error);
        expect(err).to.have.property('message', 'Failed to get Shopify access token');
        expect(err).to.have.property('responseBody', body);
        expect(err).to.have.property('statusCode', 400);
      });
    });

    it('returns an error if JSON.parse throws', function () {
      const body = '<!DOCTYPE html><html><head></head><body></body></html>';

      scope
        .post(pathname)
        .reply(200, body);

      return shopifyToken.getAccessToken(hostname, '123456').then(() => {
        throw new Error('Test invalidation');
      }, (err) => {
        expect(err).to.be.an.instanceof(Error);
        expect(err).to.have.property('message', 'Failed to parse the response body');
        expect(err).to.have.property('responseBody', body);
        expect(err).to.have.property('statusCode', 200);
      });
    });
  });
});


================================================
FILE: types/index.d.ts
================================================
/// <reference types="node" />
import { Agent } from 'https';

declare namespace ShopifyToken {
  export interface ShopifyTokenOptions {
    // The redirect URL for the Oauth2 flow
    redirectUri: string;
    // The Shared Secret for the app
    sharedSecret: string;
    // The API Key for the app
    apiKey: string;
    // The list of scopes
    scopes?: string | string[];
    // The request timeout
    timeout?: number;
    // API access mode
    accessMode?: string;
    // The agent used for all HTTP requests
    agent?: Agent;
  }

  export interface OfflineAccessTokenData {
    access_token: string;
    scope: string;
  }

  export interface AccessTokenAssociatedUser {
    id: number;
    first_name: string;
    last_name: string;
    email: string;
    email_verified: boolean;
    account_owner: boolean;
    locale: string;
    collaborator: boolean;
  }

  export interface OnlineAccessTokenData {
    access_token: string;
    scope: string;
    expires_in: number;
    associated_user_scope: string;
    associated_user: AccessTokenAssociatedUser;
  }
}

declare class ShopifyToken {
  /**
   * Create a ShopifyToken instance.
   *
   * @param {Object} options Configuration options
   * @param {String} options.redirectUri The redirect URL for the Oauth2 flow
   * @param {String} options.sharedSecret The Shared Secret for the app
   * @param {Array|String} [options.scopes] The list of scopes
   * @param {String} options.apiKey The API Key for the app
   * @param {String} [options.accessMode] The API access mode
   * @param {Number} [options.timeout] The request timeout
   * @param {Agent} [options.agent] The agent used for all HTTP requests
   */
  constructor(options: ShopifyToken.ShopifyTokenOptions);
  /**
   * Generate a random nonce.
   *
   * @return {String} The random nonce
   * @public
   */
  generateNonce(): string;
  /**
   * Build the authorization URL.
   *
   * @param {String} shop The shop name
   * @param {Array|String} [scopes] The list of scopes
   * @param {String} [nonce] The nonce
   * @param {String} [accessMode] The API access mode
   * @return {String} The authorization URL
   * @public
   */
  generateAuthUrl(
    shop: string,
    scopes?: string | string[],
    nonce?: string,
    accessMode?: string
  ): string;
  /**
   * Verify the hmac returned by Shopify.
   *
   * @param {Object} query The parsed query string
   * @return {Boolean} `true` if the hmac is valid, else `false`
   * @public
   */
  verifyHmac(query: any): boolean;
  /**
   * Request an access token.
   *
   * @param {String} shop The hostname of the shop, e.g. foo.myshopify.com
   * @param {String} code The authorization code
   * @return {Promise} Promise which is fulfilled with an access token and
   *     additional data
   * @public
   */
  getAccessToken(
    shop: string,
    code: string
  ): Promise<
    ShopifyToken.OfflineAccessTokenData | ShopifyToken.OnlineAccessTokenData
  >;
}

export = ShopifyToken;
Download .txt
gitextract_fvi5qwv6/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── example/
│   ├── README.md
│   ├── config.json
│   ├── index.js
│   └── package.json
├── index.js
├── package.json
├── test.js
└── types/
    └── index.d.ts
Download .txt
SYMBOL INDEX (14 symbols across 3 files)

FILE: index.js
  function timingSafeEqual (line 37) | function timingSafeEqual(a, b) {
  class ShopifyToken (line 50) | class ShopifyToken {
    method constructor (line 63) | constructor(options) {
    method generateNonce (line 88) | generateNonce() {
    method generateAuthUrl (line 102) | generateAuthUrl(shop, scopes, nonce, accessMode) {
    method verifyHmac (line 132) | verifyHmac(query) {
    method getAccessToken (line 167) | getAccessToken(shop, code) {

FILE: test.js
  method read (line 275) | read() {}
  method write (line 276) | write(chunk, encoding, callback) {

FILE: types/index.d.ts
  type ShopifyTokenOptions (line 5) | interface ShopifyTokenOptions {
  type OfflineAccessTokenData (line 22) | interface OfflineAccessTokenData {
  type AccessTokenAssociatedUser (line 27) | interface AccessTokenAssociatedUser {
  type OnlineAccessTokenData (line 38) | interface OnlineAccessTokenData {
  class ShopifyToken (line 47) | class ShopifyToken {
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (36K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 509,
    "preview": "name: CI\n\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        no"
  },
  {
    "path": ".gitignore",
    "chars": 24,
    "preview": "node_modules/\ncoverage/\n"
  },
  {
    "path": ".npmrc",
    "chars": 19,
    "preview": "package-lock=false\n"
  },
  {
    "path": "LICENSE",
    "chars": 1055,
    "preview": "Copyright (c) 2016 Luigi Pinca\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this sof"
  },
  {
    "path": "README.md",
    "chars": 6233,
    "preview": "# shopify-token\n\n[![Version npm][npm-shopify-token-badge]][npm-shopify-token]\n[![Build Status][ci-shopify-token-badge]]["
  },
  {
    "path": "example/README.md",
    "chars": 364,
    "preview": "# Example\n\nThis example shows you how you can use `shopify-token` with `express`.\n\nTo run it, edit the `config.json` fil"
  },
  {
    "path": "example/config.json",
    "chars": 132,
    "preview": "{\n  \"redirectUri\": \"http://localhost:8080/callback\",\n  \"sharedSecret\": \"APP_SECRET\",\n  \"apiKey\": \"APP_KEY\",\n  \"shop\": \"S"
  },
  {
    "path": "example/index.js",
    "chars": 1772,
    "preview": "'use strict';\n\nconst session = require('express-session');\nconst express = require('express');\n\nconst ShopifyToken = req"
  },
  {
    "path": "example/package.json",
    "chars": 290,
    "preview": "{\n  \"name\": \"shopify-token-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"keywords\": [\n  "
  },
  {
    "path": "index.js",
    "chars": 6398,
    "preview": "'use strict';\n\nconst crypto = require('crypto');\nconst https = require('https');\nconst url = require('url');\n\n/**\n * Enc"
  },
  {
    "path": "package.json",
    "chars": 768,
    "preview": "{\n  \"name\": \"shopify-token\",\n  \"version\": \"4.1.0\",\n  \"description\": \"Get an OAuth 2.0 access token for the Shopify API w"
  },
  {
    "path": "test.js",
    "chars": 13121,
    "preview": "describe('shopify-token', function () {\n  'use strict';\n\n  const expect = require('chai').expect;\n  const stream = requi"
  },
  {
    "path": "types/index.d.ts",
    "chars": 2966,
    "preview": "/// <reference types=\"node\" />\nimport { Agent } from 'https';\n\ndeclare namespace ShopifyToken {\n  export interface Shopi"
  }
]

About this extraction

This page contains the full source code of the lpinca/shopify-token GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (32.9 KB), approximately 8.9k tokens, and a symbol index with 14 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!