New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@shopify/shopify-api

Package Overview
Dependencies
Maintainers
19
Versions
100
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@shopify/shopify-api - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

dist/utils/delete-current-session.d.ts

7

CHANGELOG.md

@@ -12,5 +12,12 @@ # Changelog

### Fixed
## [0.2.0] - 2021-01-13
- Preserve the OAuth cookie session for a few seconds so SPA can perform their initial load using it [#70](https://github.com/shopify/shopify_ts_api/pull/70)
- Session fetches now return `undefined` when a session is not available [#64](https://github.com/shopify/shopify_ts_api/pull/64)
- Add `deleteCurrentSession` utils method [#60](https://github.com/shopify/shopify_ts_api/pull/60)
## [0.1.0] - 2020-12-17
- Beta release
## [0.0.1] - 2020-12-17

@@ -17,0 +24,0 @@

3

dist/auth/index.d.ts

@@ -8,2 +8,5 @@ /// <reference types="node" />

validateAuthCallback(request: import("http").IncomingMessage, response: import("http").ServerResponse, query: import("./types").AuthQuery): Promise<void>;
getCookieSessionId(request: import("http").IncomingMessage, response: import("http").ServerResponse): string | undefined;
getJwtSessionId(shop: string, userId: string): string;
getOfflineSessionId(shop: string): string;
};

@@ -10,0 +13,0 @@ Session: typeof Session;

@@ -28,4 +28,24 @@ import http from 'http';

validateAuthCallback(request: http.IncomingMessage, response: http.ServerResponse, query: AuthQuery): Promise<void>;
/**
* Loads the current session id from the session cookie.
*
* @param request HTTP request object
* @param response HTTP response object
*/
getCookieSessionId(request: http.IncomingMessage, response: http.ServerResponse): string | undefined;
/**
* Builds a JWT session id from the current shop and user.
*
* @param shop Shopify shop domain
* @param userId Current actor id
*/
getJwtSessionId(shop: string, userId: string): string;
/**
* Builds an offline session id for the given shop.
*
* @param shop Shopify shop domain
*/
getOfflineSessionId(shop: string): string;
};
export { ShopifyOAuth };
//# sourceMappingURL=oauth.d.ts.map

@@ -7,5 +7,5 @@ "use strict";

var cookies_1 = tslib_1.__importDefault(require("cookies"));
var querystring_1 = tslib_1.__importDefault(require("querystring"));
var context_1 = require("../../context");
var utils_1 = tslib_1.__importDefault(require("../../utils"));
var querystring_1 = tslib_1.__importDefault(require("querystring"));
var session_1 = require("../session");

@@ -40,3 +40,3 @@ var http_client_1 = require("../../clients/http_client");

state = utils_1.default.nonce();
session = new session_1.Session(isOnline ? uuid_1.v4() : shop + '_offline');
session = new session_1.Session(isOnline ? uuid_1.v4() : this.getOfflineSessionId(shop));
session.shop = shop;

@@ -79,3 +79,3 @@ session.state = state;

return tslib_1.__awaiter(this, void 0, void 0, function () {
var cookies, currentSession, body, postParams, client, postResponse, responseBody, access_token, scope, rest, sessionExpiration, responseBody;
var cookies, currentSession, sessionCookie, body, postParams, client, postResponse, responseBody, access_token, scope, rest, sessionExpiration, responseBody, onlineInfo, jwtSessionId, jwtSession;
return tslib_1.__generator(this, function (_a) {

@@ -89,5 +89,10 @@ switch (_a.label) {

});
return [4 /*yield*/, utils_1.default.loadCurrentSession(request, response, true)];
currentSession = undefined;
sessionCookie = this.getCookieSessionId(request, response);
if (!sessionCookie) return [3 /*break*/, 2];
return [4 /*yield*/, context_1.Context.loadSession(sessionCookie)];
case 1:
currentSession = _a.sent();
_a.label = 2;
case 2:
if (!currentSession) {

@@ -111,3 +116,3 @@ throw new ShopifyErrors.SessionNotFound("Cannot complete OAuth process. No session found for the specified shop url: " + query.shop);

return [4 /*yield*/, client.post(postParams)];
case 2:
case 3:
postResponse = _a.sent();

@@ -122,8 +127,2 @@ if (currentSession.isOnline) {

currentSession.onlineAccesInfo = rest;
cookies.set(ShopifyOAuth.SESSION_COOKIE_NAME, currentSession.id, {
signed: true,
expires: sessionExpiration,
sameSite: 'none',
secure: true,
});
}

@@ -135,5 +134,21 @@ else {

}
return [4 /*yield*/, context_1.Context.storeSession(currentSession)];
case 3:
// If app is embedded or this is an offline session, we're no longer intereseted in the cookie
cookies.set(ShopifyOAuth.SESSION_COOKIE_NAME, currentSession.id, {
signed: true,
expires: context_1.Context.IS_EMBEDDED_APP || !currentSession.isOnline ? new Date() : currentSession.expires,
sameSite: 'none',
secure: true,
});
if (!(context_1.Context.IS_EMBEDDED_APP && currentSession.isOnline)) return [3 /*break*/, 5];
onlineInfo = currentSession.onlineAccesInfo;
jwtSessionId = this.getJwtSessionId(currentSession.shop, '' + onlineInfo.associated_user.id);
jwtSession = session_1.Session.cloneSession(currentSession, jwtSessionId);
return [4 /*yield*/, context_1.Context.storeSession(jwtSession)];
case 4:
_a.sent();
currentSession.expires = new Date(Date.now() + 30000);
_a.label = 5;
case 5: return [4 /*yield*/, context_1.Context.storeSession(currentSession)];
case 6:
_a.sent();
return [2 /*return*/];

@@ -144,2 +159,32 @@ }

},
/**
* Loads the current session id from the session cookie.
*
* @param request HTTP request object
* @param response HTTP response object
*/
getCookieSessionId: function (request, response) {
var cookies = new cookies_1.default(request, response, {
secure: true,
keys: [context_1.Context.API_SECRET_KEY],
});
return cookies.get(this.SESSION_COOKIE_NAME, { signed: true });
},
/**
* Builds a JWT session id from the current shop and user.
*
* @param shop Shopify shop domain
* @param userId Current actor id
*/
getJwtSessionId: function (shop, userId) {
return shop + "_" + userId;
},
/**
* Builds an offline session id for the given shop.
*
* @param shop Shopify shop domain
*/
getOfflineSessionId: function (shop) {
return "offline_" + shop;
},
};

@@ -146,0 +191,0 @@ exports.ShopifyOAuth = ShopifyOAuth;

2

dist/auth/session/session_storage.d.ts

@@ -17,3 +17,3 @@ import { Session } from './session';

*/
loadSession(id: string): Promise<Session | null>;
loadSession(id: string): Promise<Session | undefined>;
/**

@@ -20,0 +20,0 @@ * Deletes a session from storage.

@@ -15,5 +15,5 @@ import { OnlineAccessInfo } from '../types';

constructor(id: string);
static cloneSession(session: Session, newId: string): Promise<Session>;
static cloneSession(session: Session, newId: string): Session;
}
export { Session };
//# sourceMappingURL=session.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Session = void 0;
var tslib_1 = require("tslib");
/**

@@ -13,16 +12,11 @@ * Stores App information from logged in merchants so they can make authenticated requests to the Admin API.

Session.cloneSession = function (session, newId) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var newSession;
return tslib_1.__generator(this, function (_a) {
newSession = new Session(newId);
newSession.shop = session.shop;
newSession.state = session.state;
newSession.scope = session.scope;
newSession.expires = session.expires;
newSession.isOnline = session.isOnline;
newSession.accessToken = session.accessToken;
newSession.onlineAccesInfo = session.onlineAccesInfo;
return [2 /*return*/, newSession];
});
});
var newSession = new Session(newId);
newSession.shop = session.shop;
newSession.state = session.state;
newSession.scope = session.scope;
newSession.expires = session.expires;
newSession.isOnline = session.isOnline;
newSession.accessToken = session.accessToken;
newSession.onlineAccesInfo = session.onlineAccesInfo;
return newSession;
};

@@ -29,0 +23,0 @@ return Session;

@@ -5,9 +5,9 @@ import { Session } from '../session';

readonly storeCallback: (session: Session) => boolean;
readonly loadCallback: (id: string) => Session | null;
readonly loadCallback: (id: string) => Session | undefined;
readonly deleteCallback: (id: string) => boolean;
constructor(storeCallback: (session: Session) => boolean, loadCallback: (id: string) => Session | null, deleteCallback: (id: string) => boolean);
constructor(storeCallback: (session: Session) => boolean, loadCallback: (id: string) => Session | undefined, deleteCallback: (id: string) => boolean);
storeSession(session: Session): Promise<boolean>;
loadSession(id: string): Promise<Session | null>;
loadSession(id: string): Promise<Session | undefined>;
deleteSession(id: string): Promise<boolean>;
}
//# sourceMappingURL=custom.d.ts.map

@@ -6,5 +6,5 @@ import { Session } from '../session';

storeSession(session: Session): Promise<boolean>;
loadSession(id: string): Promise<Session | null>;
loadSession(id: string): Promise<Session | undefined>;
deleteSession(id: string): Promise<boolean>;
}
//# sourceMappingURL=memory.d.ts.map

@@ -20,3 +20,3 @@ "use strict";

return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, this.sessions[id] || null];
return [2 /*return*/, this.sessions[id] || undefined];
});

@@ -23,0 +23,0 @@ });

@@ -22,3 +22,3 @@ import { Session, SessionStorage } from './auth/session';

*/
loadSession(id: string): Promise<Session | null>;
loadSession(id: string): Promise<Session | undefined>;
/**

@@ -25,0 +25,0 @@ * Deletes a session using the assigned strategy.

@@ -10,2 +10,5 @@ /// <reference types="node" />

validateAuthCallback(request: import("http").IncomingMessage, response: import("http").ServerResponse, query: import("./types").AuthQuery): Promise<void>;
getCookieSessionId(request: import("http").IncomingMessage, response: import("http").ServerResponse): string | undefined;
getJwtSessionId(shop: string, userId: string): string;
getOfflineSessionId(shop: string): string;
};

@@ -12,0 +15,0 @@ Session: typeof import("./auth/session");

@@ -28,5 +28,4 @@ import { SessionStorage } from './auth/session';

}
export * from './webhooks/types';
export * from './auth/types';
export * from './clients/types';
//# sourceMappingURL=types.d.ts.map

@@ -24,4 +24,3 @@ "use strict";

})(ShopifyHeader = exports.ShopifyHeader || (exports.ShopifyHeader = {}));
tslib_1.__exportStar(require("./webhooks/types"), exports);
tslib_1.__exportStar(require("./auth/types"), exports);
tslib_1.__exportStar(require("./clients/types"), exports);

@@ -5,10 +5,8 @@ /// <reference types="node" />

/**
* Loads the current user's session, based on the given request.
* Loads the current user's session, based on the given request and response.
*
* @param req Current HTTP request
* @param res Current HTTP response
* @param isOauthCall Whether to expect an ongoing OAuth flow. If there is one and a session token is given,
* the session will be converted for future uses. Generally not necessary if using default OAuth
*/
export default function loadCurrentSession(request: http.IncomingMessage, response: http.ServerResponse, isOauthCall?: boolean): Promise<Session | null>;
export default function loadCurrentSession(request: http.IncomingMessage, response: http.ServerResponse): Promise<Session | undefined>;
//# sourceMappingURL=load-current-session.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var cookies_1 = tslib_1.__importDefault(require("cookies"));
var context_1 = require("../context");
var ShopifyErrors = tslib_1.__importStar(require("../error"));
var oauth_1 = require("../auth/oauth/oauth");
var session_1 = require("../auth/session");
var decode_session_token_1 = tslib_1.__importDefault(require("./decode-session-token"));
/**
* Loads the current user's session, based on the given request.
* Loads the current user's session, based on the given request and response.
*
* @param req Current HTTP request
* @param res Current HTTP response
* @param isOauthCall Whether to expect an ongoing OAuth flow. If there is one and a session token is given,
* the session will be converted for future uses. Generally not necessary if using default OAuth
*/
function loadCurrentSession(request, response, isOauthCall) {
if (isOauthCall === void 0) { isOauthCall = false; }
function loadCurrentSession(request, response) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var cookies, sessionCookie, session, authHeader, matches, jwtPayload, oauthSession;
var session, authHeader, matches, jwtPayload, jwtSessionId, sessionCookie;
return tslib_1.__generator(this, function (_a) {

@@ -26,43 +21,25 @@ switch (_a.label) {

context_1.Context.throwIfUninitialized();
cookies = new cookies_1.default(request, response, {
secure: true,
keys: [context_1.Context.API_SECRET_KEY],
});
sessionCookie = cookies.get(oauth_1.ShopifyOAuth.SESSION_COOKIE_NAME, { signed: true });
session = null;
if (!(!isOauthCall && context_1.Context.IS_EMBEDDED_APP)) return [3 /*break*/, 7];
session = undefined;
if (!context_1.Context.IS_EMBEDDED_APP) return [3 /*break*/, 2];
authHeader = request.headers['authorization'];
if (!authHeader) return [3 /*break*/, 6];
if (!authHeader) return [3 /*break*/, 2];
matches = authHeader.match(/^Bearer (.+)$/);
if (!matches) {
throw new ShopifyErrors.MissingJwtTokenError("Missing Bearer token in authorization header");
throw new ShopifyErrors.MissingJwtTokenError('Missing Bearer token in authorization header');
}
jwtPayload = decode_session_token_1.default(matches[1]);
return [4 /*yield*/, context_1.Context.loadSession(jwtPayload.sid)];
jwtSessionId = oauth_1.ShopifyOAuth.getJwtSessionId(jwtPayload.dest.replace(/^https:\/\//, ''), jwtPayload.sub);
return [4 /*yield*/, context_1.Context.loadSession(jwtSessionId)];
case 1:
session = _a.sent();
if (!(!session && sessionCookie)) return [3 /*break*/, 6];
_a.label = 2;
case 2:
if (!!session) return [3 /*break*/, 4];
sessionCookie = oauth_1.ShopifyOAuth.getCookieSessionId(request, response);
if (!sessionCookie) return [3 /*break*/, 4];
return [4 /*yield*/, context_1.Context.loadSession(sessionCookie)];
case 2:
oauthSession = _a.sent();
if (!oauthSession) return [3 /*break*/, 6];
return [4 /*yield*/, session_1.Session.cloneSession(oauthSession, jwtPayload.sid)];
case 3:
session = _a.sent();
session.expires = new Date(jwtPayload.exp * 1000);
return [4 /*yield*/, context_1.Context.storeSession(session)];
case 4:
_a.sent();
return [4 /*yield*/, context_1.Context.deleteSession(oauthSession.id)];
case 5:
_a.sent();
_a.label = 6;
case 6: return [3 /*break*/, 9];
case 7:
if (!sessionCookie) return [3 /*break*/, 9];
return [4 /*yield*/, context_1.Context.loadSession(sessionCookie)];
case 8:
session = _a.sent();
_a.label = 9;
case 9: return [2 /*return*/, session];
_a.label = 4;
case 4: return [2 /*return*/, session];
}

@@ -69,0 +46,0 @@ });

@@ -1,2 +0,2 @@

export declare const SHOPIFY_APP_DEV_KIT_VERSION = "0.1.0";
export declare const SHOPIFY_APP_DEV_KIT_VERSION = "0.2.0";
//# sourceMappingURL=version.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SHOPIFY_APP_DEV_KIT_VERSION = void 0;
exports.SHOPIFY_APP_DEV_KIT_VERSION = '0.1.0';
exports.SHOPIFY_APP_DEV_KIT_VERSION = '0.2.0';
/// <reference types="node" />
import { StatusCode } from '@shopify/network';
import { ApiVersion } from '../types';
import { Topic } from './types';
export declare enum DeliveryMethod {

@@ -11,3 +10,4 @@ Http = "http",

export interface RegisterOptions {
topic: Topic;
/** See https://shopify.dev/docs/admin-api/graphql/reference/events/webhooksubscriptiontopic for available topics */
topic: string;
path: string;

@@ -14,0 +14,0 @@ shop: string;

{
"name": "@shopify/shopify-api",
"version": "0.1.0",
"version": "0.2.0",
"description": "Shopify TypeScript API to support core API functionality (auth, graphql proxy, webhooks)",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -5,460 +5,44 @@ # `@shopify/shopify-api`

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md)
<!-- [![npm version](https://badge.fury.io/js/%40shopify%2Fkoa-shopify-auth.svg)](https://badge.fury.io/js/%40shopify%2Fshopify-api) -->
[![npm version](https://badge.fury.io/js/%40shopify%2Fshopify-api.svg)](https://badge.fury.io/js/%40shopify%2Fshopify-api)
TypeScript API supporting authentication, graphql and REST client, and registration/receipt of webhooks for [Shopify](https://www.shopify.ca/) applications.
This library provides support for TypeScript/JavaScript [Shopify](https://www.shopify.com) apps to access the [Shopify Admin API](https://shopify.dev/docs/admin-api), by making it easier to perform the following actions:
# Usage and examples
- Creating [online](https://shopify.dev/concepts/about-apis/authentication#online-access) or [offline](https://shopify.dev/concepts/about-apis/authentication#offline-access) access tokens for the Admin API via OAuth
- Making requests to the [REST API](https://shopify.dev/docs/admin-api/rest/reference)
- Making requests to the [GraphQL API](https://shopify.dev/docs/admin-api/graphql/reference)
- Register/process webhooks
**Requirements**
This library can be used in any application that has a Node.js backend, since it doesn't rely on any specific framework—you can include it alongside your preferred stack and only use the features that you need to build your app.
# Requirements
To follow these usage guides, you will need to:
- have a basic understanding of [Node](https://nodejs.org) and of [TypeScript](https://typescriptlang.org)
- have a basic understanding of [Node.js](https://nodejs.org) and of [TypeScript](https://typescriptlang.org)
- have a Shopify Partner account and development store
- _OR_ have a test store where you can create a private app
- have a private or custom app already setup in your test store or partner account
- have a private or custom app already set up in your test store or partner account
- use [ngrok](https://ngrok.com), in order to create a secure tunnel to your app running on your localhost
- add the `ngrok` url and the appropriate redirect for your OAuth callback route to your app settings
- add the `ngrok` URL and the appropriate redirect for your OAuth callback route to your app settings
- have [yarn](https://yarnpkg.com) installed
**Environment**
<!-- Make sure this section is in sync with docs/index.md -->
# Getting started
You'll need your application to load the API secret and API secret key (found when you create an app in your Partner Account). The mechanism is entirely up to you. For example, you could use a library like `dotenv` to read them from a `.env` file, or set environment variables with the values. However you do it, be sure to NOT save the API secret and key in GitHub or other code repository where others can view them.
## Express
You can follow our [getting started guide](docs/index.md), which will provide instructions on how to create an app using plain Node.js code, or the [Express](https://expressjs.com/) framework. Both examples are written in Typescript.
For this guide, basic understanding of the [Express](https://expressjs.com/) app framework is required.
**Install dependencies**
- This step will generate your app's `package.json` and install the following necessary dependencies:
- `@shopify/shopify-api` - this library
- `express` - the Express framework
- `dotenv` - tool to read from `.env` files
- `typescript` - TypeScript language
- `@types/express` - Express types
```shell
$ yarn init -y
$ yarn add @shopify/shopify-api express
$ yarn add --dev dotenv typescript @types/express
```
**Setup environment**
Begin by placing the following in an `.env` file at the root of your project:
- Your API secret
- Your API secret key
- Your app's host, without the protocol prefix (in this case we used an `ngrok` tunnel to provide a secure connection to our localhost)
- Your test store url
- Your app's required scopes
```
SHOP={dev store url}
API_KEY={api key}
API_SECRET_KEY={api secret key}
SCOPES={scopes}
HOST={your app's host, we used ngrok}
```
**Setup base files**
- Add the following `tsconfig.json` in the root of your project:
```json
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*"
]
}
},
"include": [
"src/**/*"
]
}
```
- Add the following `scripts` to your `package.json`:
```json
"scripts": {
"build": "npx tsc",
"prestart": "yarn run build",
"start": "node dist/index.js"
},
```
- Create `src/index.ts`
**Add imports, environment variables, and setup `Context`**
```ts
// src/index.ts
import express from 'express';
import Shopify, { ApiVersion, AuthQuery } from '@shopify/shopify-api';
require('dotenv').config()
const app = express()
const { API_KEY, API_SECRET_KEY, SCOPES, SHOP, HOST } = process.env
Shopify.Context.initialize({
API_KEY,
API_SECRET_KEY,
SCOPES: [SCOPES],
HOST_NAME: HOST,
IS_EMBEDDED_APP: {boolean},
API_VERSION: ApiVersion.{version} // all supported versions are available, as well as "unstable" and "unversioned"
})
// the rest of the example code goes here
app.listen(3000, () => {
console.log('your app is now listening on port 3000')
})
```
**Add a route to start OAuth**
- This route will be used to begin the OAuth process, by using the library's built-in method and redirecting to the returned URL
- The `beginAuth` function takes in the `request` and `response` objects, along with the target shop _(string)_, redirect route _(string)_, and whether or not you are requesting "online access" _(boolean)_
- Your route callback should be `async` in order to `await` the return value, since `beginAuth` returns a `Promise`
```ts
app.get('/oauth/begin', async (req, res) => {
let authRoute = await Shopify.Auth.OAuth.beginAuth(req, res, SHOP, '/auth/callback', true)
return res.redirect(authRoute)
})
```
**Add your OAuth callback route**
- This route will use the built-in `validateAuthCallback` method
- This method takes in the `request` and `response` object, as well as the `req.query` object
- This method _has no return value_, so you should `catch` any errors it may throw
- This method also returns a `Promise` so you should use `async/await` here as well
```ts
app.get('/auth/callback', async (req, res) => {
try {
await Shopify.Auth.OAuth.validateAuthCallback(req, res, req.query as unknown as AuthQuery); // req.query must be cast to unkown and then AuthQuery in order to be accepted
} catch (error) {
console.error(error); // in practice these should be handled more gracefully
}
return res.redirect('/'); // wherever you want your user to end up after OAuth completes
})
```
**Make a REST API call**
- Once OAuth is complete, we can use the built-in `RestClient` to make an API call
- Create an instance of `RestClient` using the current shop URL and session `accessToken`
- Use `client.get` to request some `products`
```ts
app.get('/shopify/rest-call', async (req, res) => {
const session = await Shopify.Utils.loadCurrentSession(req, res) // load the current session to get the `accessToken`
const client = new Shopify.Clients.Rest.RestClient(session.shop, session.accessToken) // create a new client for the specified shop
const products = await client.get({ // use client.get to request the REST endpoint you need, in this case "products"
path: 'products'
})
res.send(products) // do something with the returned data
})
```
**Make a GraphQL API call**
- You can also use the `GraphQLClient` to make requests to the GraphQL API in a similar way
- Create an instance of `GraphQLClient` using the current shop URL and session `accessToken`
- Use `client.query` to make your request, passing your query as `data`
```ts
app.get('/shopify/graphql-call', async (req, res) => {
const session = await Shopify.Utils.loadCurrentSession(req, res) // load the current session to get the `accessToken`
const client = new Shopify.Clients.Graphql.GraphqlClient(session.shop, session.accessToken) // GraphQLClient accepts the same arguments as RestClient
const products = await client.query({ // use client.query and pass your query as `data`
data: `{
products (first: 10) {
edges {
node {
id
title
bodyHtml
}
}
}
}`
})
res.send(products) // do something with the returned data
})
```
**Running your app**
- Start your `ngrok` tunnel and add the displayed `ngrok` url to the app setup in your admin, along with the redirect route
```shell
$ ngrok http 3000
```
- Run `yarn start` and you should have your app running on your specified `localhost`
- In your browser, navigate to `{your ngrok address}/oauth/begin` to begin OAuth
- When OAuth completes, navigate to your REST and GraphQL endpoints to see the requested data being returned
## Node (no framework)
**Install dependencies**
- This step will generate your app's `package.json` and install the following necessary dependencies:
- `@shopify/shopify-api` - this library
- `dotenv` - tool to read from `.env` files
- `typescript` - TypeScript language
- `@types/node` - Node types
```shell
$ yarn init -y
$ yarn add @shopify/shopify-api
$ yarn add --dev dotenv typescript @types/node
```
**Setup environment**
Begin by placing the following in an `.env` file at the root of your project:
- Your API secret
- Your API secret key
- Your app's host, without the protocol prefix (in this case we used an `ngrok` tunnel to provide a secure connection to our localhost)
- Your test store url
- Your app's required scopes
```
SHOP={dev store url}
API_KEY={api key}
API_SECRET_KEY={api secret key}
SCOPES={scopes}
HOST={your app's host, we used ngrok}
```
**Setup base files**
- Add the following `tsconfig.json` in the root of your project:
```json
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*"
]
}
},
"include": [
"src/**/*"
]
}
```
- Add the following `scripts` to your `package.json`:
```json
"scripts": {
"build": "npx tsc",
"prestart": "yarn run build",
"start": "node dist/index.js"
},
```
- Create `src/index.ts`
**Add imports, environment variables, and setup `Context`**
```typescript
// src/index.ts
import http from 'http';
import url from 'url';
import querystring from 'querystring';
import Shopify, { ApiVersion, AuthQuery } from '@shopify/shopify-api';
require('dotenv').config()
const { API_KEY, API_SECRET_KEY, SCOPES, SHOP, HOST } = process.env
Shopify.Context.initialize({
API_KEY,
API_SECRET_KEY,
SCOPES: [SCOPES],
HOST_NAME: HOST,
IS_EMBEDDED_APP: {boolean},
API_VERSION: ApiVersion.{version} // all supported versions are available, as well as "unstable" and "unversioned"
});
```
**Create a basic Node router**
```typescript
:
async function onRequest(request: http.IncomingMessage, response: http.ServerResponse): Promise<void> {
const { headers, url: req_url } = request;
const pathName: string | null = url.parse(req_url).pathname;
const queryString: string = (String)(url.parse(req_url).query);
const query: Record<string, any> = querystring.parse(queryString);
if (pathName === '/') {
// check if we're logged in/authorized
const currentSession = await Shopify.Utils.loadCurrentSession(request, response);
if(!currentSession) {
// not logged in, redirect to login
response.writeHead(302, { 'Location': `/login` });
response.end();
} else {
// do something amazing with your application!
}
return;
} // end of if(pathName === '/')
} // end of onRequest()
http.createServer(onRequest).listen(3000);
```
**Add a route to start OAuth**
The route for starting the OAuth process (in this case `/login`) will use the library's `beginAuth` method. The `beginAuth` method takes in the request and response objects (from the `http` server), along with the target shop (string), redirect route (string), and whether or not you are requesting "online access" (boolean). The method will return a URI that will be used for redirecting the user to the Shopify Authentication screen.
```typescript
:
} // end of if(pathName === '/')
if (pathName === '/login') {
// process login action
try {
const authRoute = await Shopify.Auth.OAuth.beginAuth(request, response, SHOP, callbackPath);
response.writeHead(302, { 'Location': authRoute });
response.end();
}
catch (e) {
console.log(e);
response.writeHead(500);
if (e instanceof Shopify.Errors.ShopifyError) {
response.end(e.message);
}
else {
response.end(`Failed to complete OAuth process: ${e.message}`);
}
}
return;
} // end of if (pathName === '/login')
} // end of onRequest()
http.createServer(onRequest).listen(3000);
```
**Add your OAuth callback route**
After the app is authenticated with Shopify, the Shopify platform will send a request back to your app using this route (which you provided as a parameter to `beginAuth`, above). Your app will now use the provided `validateAuthCallback` method to finalized the OAuth process.
```typescript
:
} // end of if (pathName === '/login')
if (pathName === callbackPath) {
try {
await Shopify.Auth.OAuth.validateAuthCallback(request, response, query as AuthQuery);
// all good, redirect to '/'
response.writeHead(302, { 'Location': '/' });
response.end();
}
catch (e) {
console.log(e);
response.writeHead(500);
if (e instanceof Shopify.Errors.ShopifyError) {
response.end(e.message);
}
else {
response.end(`Failed to complete OAuth process: ${e.message}`);
}
}
return;
} // end of if (pathName === 'callbackPath')
} // end of onRequest()
http.createServer(onRequest).listen(3000);
```
**Register a Webhook**
If your application's functionality depends on knowing when events occur on a given store, you need to register a Webhook! You should register your webhooks after the OAuth process completes.
```typescript
:
} // end of if (pathName === '/login')
if (pathName === callbackPath) {
try {
await Shopify.Auth.OAuth.validateAuthCallback(request, response, query as AuthQuery);
// all good, register any required webhooks
const handleWebhookRequest = (topic: string, shop_domain: string, webhookRequestBody: Buffer) => {
// this handler is triggered when a webhook is sent by the Shopify platform to your application
}
const currentSession = await Shopify.Utils.loadCurrentSession(request, response, false);
const resp = await Shopify.Webhooks.Registry.register({
path: '/webhooks',
topic: 'PRODUCTS_CREATE',
accessToken: currentSession.accessToken,
shop: currentSession.shop,
apiVersion: Context.API_VERSION,
webhookHandler: handleWebhookRequest
});
response.writeHead(302, { 'Location': '/' });
response.end();
}
catch (e) {
:
```
**Process a Webhook**
To process a webhook, you need to listen on the route(s) you provided during the Webhook registration process, then call the appropriate handler. The library provides a convenient `process` method which takes care of calling the correct handler for the registered Webhook topics.
```typescript
:
} // end of if (pathName === 'callbackPath')
if (Shopify.Webhooks.Registry.isWebhookPath(pathName)) {
let data: Buffer[] = [];
request.on('data', function (chunk: Buffer) {
data.push(chunk)
}).on('end', function () {
const buffer: Buffer = Buffer.concat(data);
try {
const result = Shopify.Webhooks.Registry.process({ headers: headers, body: buffer });
response.writeHead(result.statusCode, result.headers);
response.end();
}
catch (e) {
console.log(e);
response.writeHead(500);
if (e instanceof Shopify.Errors.ShopifyError) {
response.end(e.message);
}
else {
response.end(`Failed to complete webhook processing: ${e.message}`);
}
}
});
return;
} // end of if (Shopify.Webhooks.Registry.isWebhookPath(pathName))
} // end of onRequest()
http.createServer(onRequest).listen(3000);
```
**Running your app**
- Start your `ngrok` tunnel and add the displayed `ngrok` url to the app setup in your admin, along with the redirect route
```shell
$ ngrok http 3000
```
- Run `yarn start` and you should have your app running on your specified `localhost`
- In your browser, navigate to `{your ngrok address}/login` to begin OAuth
- [Getting started](docs/getting_started.md)
- [Install dependencies](docs/getting_started.md#install-dependencies)
- [Set up base files](docs/getting_started.md#set-up-base-files)
- [Set up environment](docs/getting_started.md#set-up-environment)
- [Set up Context](docs/getting_started.md#set-up-context)
- [Running your app](docs/getting_started.md#running-your-app)
- [Performing OAuth](docs/usage/oauth.md)
- [Add a route to start OAuth](docs/usage/oauth.md#add-a-route-to-start-oauth)
- [Add your OAuth callback route](docs/usage/oauth.md#add-your-oauth-callback-route)
- [Make a REST API call](docs/usage/rest.md)
- [Make a GraphQL API call](docs/usage/graphql.md)
- [Webhooks](docs/usage/webhooks.md)
- [Register a Webhook](docs/usage/webhooks.md#register-a-webhook)
- [Process a Webhook](docs/usage/webhooks.md#process-a-webhook)
- [Known issues and caveats](docs/issues.md)
- [Notes on session handling](docs/issues.md#notes-on-session-handling)

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc