@shopify/shopify-app-remix 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +44 -0
  2. package/build/cjs/server/adapters/node/index.js +1 -0
  3. package/build/cjs/server/authenticate/admin/authenticate.js +19 -14
  4. package/build/cjs/server/authenticate/admin/billing/cancel.js +1 -1
  5. package/build/cjs/server/authenticate/admin/billing/check.js +39 -0
  6. package/build/cjs/server/authenticate/admin/billing/require.js +1 -1
  7. package/build/cjs/server/authenticate/admin/helpers/create-admin-api-context.js +2 -5
  8. package/build/cjs/server/authenticate/admin/helpers/handle-client-error.js +8 -4
  9. package/build/cjs/server/authenticate/admin/helpers/redirect-to-bounce-page.js +4 -2
  10. package/build/cjs/server/authenticate/admin/helpers/redirect.js +1 -5
  11. package/build/cjs/server/authenticate/admin/helpers/trigger-after-auth-hook.js +2 -2
  12. package/build/cjs/server/authenticate/admin/strategies/auth-code-flow.js +31 -4
  13. package/build/cjs/server/authenticate/admin/strategies/token-exchange.js +155 -0
  14. package/build/cjs/server/authenticate/const.js +4 -0
  15. package/build/cjs/server/authenticate/flow/authenticate.js +68 -0
  16. package/build/cjs/server/authenticate/helpers/idempotent-promise-handler.js +40 -0
  17. package/build/cjs/server/authenticate/helpers/respond-to-invalid-session-token.js +33 -0
  18. package/build/cjs/server/authenticate/login/login.js +5 -1
  19. package/build/cjs/server/authenticate/public/checkout/authenticate.js +1 -0
  20. package/build/cjs/server/authenticate/webhooks/authenticate.js +1 -1
  21. package/build/cjs/server/authenticate/webhooks/register.js +16 -0
  22. package/build/cjs/server/errors.js +9 -0
  23. package/build/cjs/server/index.js +2 -0
  24. package/build/cjs/server/shopify-app.js +9 -4
  25. package/build/cjs/server/types.js +4 -0
  26. package/build/cjs/server/unauthenticated/admin/factory.js +2 -2
  27. package/build/cjs/server/unauthenticated/storefront/factory.js +2 -2
  28. package/build/cjs/server/version.js +1 -1
  29. package/build/ts/react/components/AppProvider/AppProvider.js +1 -1
  30. package/build/ts/react/components/AppProvider/AppProvider.js.map +1 -1
  31. package/build/ts/server/__test-helpers/const.d.ts +2 -1
  32. package/build/ts/server/__test-helpers/const.d.ts.map +1 -1
  33. package/build/ts/server/__test-helpers/const.js +3 -2
  34. package/build/ts/server/__test-helpers/const.js.map +1 -1
  35. package/build/ts/server/__test-helpers/expect-document-request-headers.js.map +1 -1
  36. package/build/ts/server/__test-helpers/expect-exit-iframe.js.map +1 -1
  37. package/build/ts/server/__test-helpers/get-thrown-response.js.map +1 -1
  38. package/build/ts/server/__test-helpers/request-mock.js.map +1 -1
  39. package/build/ts/server/__test-helpers/setup-valid-session.d.ts +1 -1
  40. package/build/ts/server/__test-helpers/setup-valid-session.d.ts.map +1 -1
  41. package/build/ts/server/__test-helpers/setup-valid-session.js +5 -5
  42. package/build/ts/server/__test-helpers/setup-valid-session.js.map +1 -1
  43. package/build/ts/server/__test-helpers/test-config.d.ts +1 -0
  44. package/build/ts/server/__test-helpers/test-config.d.ts.map +1 -1
  45. package/build/ts/server/__test-helpers/test-config.js +1 -0
  46. package/build/ts/server/__test-helpers/test-config.js.map +1 -1
  47. package/build/ts/server/adapters/node/index.js.map +1 -1
  48. package/build/ts/server/authenticate/admin/authenticate.d.ts +7 -1
  49. package/build/ts/server/authenticate/admin/authenticate.d.ts.map +1 -1
  50. package/build/ts/server/authenticate/admin/authenticate.js +15 -14
  51. package/build/ts/server/authenticate/admin/authenticate.js.map +1 -1
  52. package/build/ts/server/authenticate/admin/billing/check.d.ts +6 -0
  53. package/build/ts/server/authenticate/admin/billing/check.d.ts.map +1 -0
  54. package/build/ts/server/authenticate/admin/billing/check.js +43 -0
  55. package/build/ts/server/authenticate/admin/billing/check.js.map +1 -0
  56. package/build/ts/server/authenticate/admin/billing/index.d.ts +1 -0
  57. package/build/ts/server/authenticate/admin/billing/index.d.ts.map +1 -1
  58. package/build/ts/server/authenticate/admin/billing/index.js +3 -1
  59. package/build/ts/server/authenticate/admin/billing/index.js.map +1 -1
  60. package/build/ts/server/authenticate/admin/billing/request.js.map +1 -1
  61. package/build/ts/server/authenticate/admin/billing/types.d.ts +59 -1
  62. package/build/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
  63. package/build/ts/server/authenticate/admin/helpers/create-admin-api-context.d.ts +2 -2
  64. package/build/ts/server/authenticate/admin/helpers/create-admin-api-context.d.ts.map +1 -1
  65. package/build/ts/server/authenticate/admin/helpers/create-admin-api-context.js +2 -5
  66. package/build/ts/server/authenticate/admin/helpers/create-admin-api-context.js.map +1 -1
  67. package/build/ts/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.js.map +1 -1
  68. package/build/ts/server/authenticate/admin/helpers/handle-client-error.d.ts +2 -5
  69. package/build/ts/server/authenticate/admin/helpers/handle-client-error.d.ts.map +1 -1
  70. package/build/ts/server/authenticate/admin/helpers/handle-client-error.js +6 -5
  71. package/build/ts/server/authenticate/admin/helpers/handle-client-error.js.map +1 -1
  72. package/build/ts/server/authenticate/admin/helpers/redirect-to-bounce-page.d.ts.map +1 -1
  73. package/build/ts/server/authenticate/admin/helpers/redirect-to-bounce-page.js +4 -2
  74. package/build/ts/server/authenticate/admin/helpers/redirect-to-bounce-page.js.map +1 -1
  75. package/build/ts/server/authenticate/admin/helpers/redirect-with-exitiframe.js.map +1 -1
  76. package/build/ts/server/authenticate/admin/helpers/redirect.js +0 -3
  77. package/build/ts/server/authenticate/admin/helpers/redirect.js.map +1 -1
  78. package/build/ts/server/authenticate/admin/helpers/render-app-bridge.js.map +1 -1
  79. package/build/ts/server/authenticate/admin/helpers/trigger-after-auth-hook.d.ts +2 -1
  80. package/build/ts/server/authenticate/admin/helpers/trigger-after-auth-hook.d.ts.map +1 -1
  81. package/build/ts/server/authenticate/admin/helpers/trigger-after-auth-hook.js +2 -2
  82. package/build/ts/server/authenticate/admin/helpers/trigger-after-auth-hook.js.map +1 -1
  83. package/build/ts/server/authenticate/admin/helpers/validate-shop-and-host-params.js.map +1 -1
  84. package/build/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts +4 -2
  85. package/build/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
  86. package/build/ts/server/authenticate/admin/strategies/auth-code-flow.js +26 -4
  87. package/build/ts/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
  88. package/build/ts/server/authenticate/admin/strategies/token-exchange.d.ts +17 -0
  89. package/build/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -0
  90. package/build/ts/server/authenticate/admin/strategies/token-exchange.js +163 -0
  91. package/build/ts/server/authenticate/admin/strategies/token-exchange.js.map +1 -0
  92. package/build/ts/server/authenticate/admin/strategies/types.d.ts +16 -5
  93. package/build/ts/server/authenticate/admin/strategies/types.d.ts.map +1 -1
  94. package/build/ts/server/authenticate/admin/types.d.ts +0 -4
  95. package/build/ts/server/authenticate/admin/types.d.ts.map +1 -1
  96. package/build/ts/server/authenticate/const.d.ts +3 -0
  97. package/build/ts/server/authenticate/const.d.ts.map +1 -1
  98. package/build/ts/server/authenticate/const.js +4 -1
  99. package/build/ts/server/authenticate/const.js.map +1 -1
  100. package/build/ts/server/authenticate/flow/authenticate.d.ts +5 -0
  101. package/build/ts/server/authenticate/flow/authenticate.d.ts.map +1 -0
  102. package/build/ts/server/authenticate/flow/authenticate.flow.doc.d.ts +4 -0
  103. package/build/ts/server/authenticate/flow/authenticate.flow.doc.d.ts.map +1 -0
  104. package/build/ts/server/authenticate/flow/authenticate.flow.doc.js +33 -0
  105. package/build/ts/server/authenticate/flow/authenticate.flow.doc.js.map +1 -0
  106. package/build/ts/server/authenticate/flow/authenticate.js +67 -0
  107. package/build/ts/server/authenticate/flow/authenticate.js.map +1 -0
  108. package/build/ts/server/authenticate/flow/types.d.ts +82 -0
  109. package/build/ts/server/authenticate/flow/types.d.ts.map +1 -0
  110. package/build/ts/server/authenticate/flow/types.js +3 -0
  111. package/build/ts/server/authenticate/flow/types.js.map +1 -0
  112. package/build/ts/server/authenticate/helpers/add-response-headers.js.map +1 -1
  113. package/build/ts/server/authenticate/helpers/ensure-cors-headers.js.map +1 -1
  114. package/build/ts/server/authenticate/helpers/idempotent-promise-handler.d.ts +12 -0
  115. package/build/ts/server/authenticate/helpers/idempotent-promise-handler.d.ts.map +1 -0
  116. package/build/ts/server/authenticate/helpers/idempotent-promise-handler.js +53 -0
  117. package/build/ts/server/authenticate/helpers/idempotent-promise-handler.js.map +1 -0
  118. package/build/ts/server/authenticate/helpers/index.d.ts +1 -0
  119. package/build/ts/server/authenticate/helpers/index.d.ts.map +1 -1
  120. package/build/ts/server/authenticate/helpers/index.js +1 -0
  121. package/build/ts/server/authenticate/helpers/index.js.map +1 -1
  122. package/build/ts/server/authenticate/helpers/reject-bot-request.js.map +1 -1
  123. package/build/ts/server/authenticate/helpers/respond-to-invalid-session-token.d.ts +9 -0
  124. package/build/ts/server/authenticate/helpers/respond-to-invalid-session-token.d.ts.map +1 -0
  125. package/build/ts/server/authenticate/helpers/respond-to-invalid-session-token.js +20 -0
  126. package/build/ts/server/authenticate/helpers/respond-to-invalid-session-token.js.map +1 -0
  127. package/build/ts/server/authenticate/helpers/respond-to-options-request.js.map +1 -1
  128. package/build/ts/server/authenticate/login/login.d.ts.map +1 -1
  129. package/build/ts/server/authenticate/login/login.js +6 -2
  130. package/build/ts/server/authenticate/login/login.js.map +1 -1
  131. package/build/ts/server/authenticate/public/appProxy/authenticate.js.map +1 -1
  132. package/build/ts/server/authenticate/public/checkout/authenticate.js.map +1 -1
  133. package/build/ts/server/authenticate/public/factory.js.map +1 -1
  134. package/build/ts/server/authenticate/webhooks/authenticate.js.map +1 -1
  135. package/build/ts/server/authenticate/webhooks/register.d.ts +1 -1
  136. package/build/ts/server/authenticate/webhooks/register.d.ts.map +1 -1
  137. package/build/ts/server/authenticate/webhooks/register.js +20 -1
  138. package/build/ts/server/authenticate/webhooks/register.js.map +1 -1
  139. package/build/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
  140. package/build/ts/server/boundary/error.js.map +1 -1
  141. package/build/ts/server/boundary/headers.js.map +1 -1
  142. package/build/ts/server/clients/admin/rest.js.map +1 -1
  143. package/build/ts/server/clients/admin/types.d.ts +12 -0
  144. package/build/ts/server/clients/admin/types.d.ts.map +1 -1
  145. package/build/ts/server/clients/types.d.ts +1 -3
  146. package/build/ts/server/clients/types.d.ts.map +1 -1
  147. package/build/ts/server/config-types.d.ts +3 -3
  148. package/build/ts/server/config-types.d.ts.map +1 -1
  149. package/build/ts/server/errors.d.ts +4 -0
  150. package/build/ts/server/errors.d.ts.map +1 -0
  151. package/build/ts/server/errors.js +14 -0
  152. package/build/ts/server/errors.js.map +1 -0
  153. package/build/ts/server/future/flags.d.ts +9 -0
  154. package/build/ts/server/future/flags.d.ts.map +1 -1
  155. package/build/ts/server/index.d.ts +1 -0
  156. package/build/ts/server/index.d.ts.map +1 -1
  157. package/build/ts/server/index.js +2 -0
  158. package/build/ts/server/index.js.map +1 -1
  159. package/build/ts/server/override-logger.js.map +1 -1
  160. package/build/ts/server/shopify-app.d.ts.map +1 -1
  161. package/build/ts/server/shopify-app.doc.d.ts.map +1 -1
  162. package/build/ts/server/shopify-app.doc.js +1 -0
  163. package/build/ts/server/shopify-app.doc.js.map +1 -1
  164. package/build/ts/server/shopify-app.js +8 -3
  165. package/build/ts/server/shopify-app.js.map +1 -1
  166. package/build/ts/server/types.d.ts +54 -12
  167. package/build/ts/server/types.d.ts.map +1 -1
  168. package/build/ts/server/types.js +2 -2
  169. package/build/ts/server/types.js.map +1 -1
  170. package/build/ts/server/unauthenticated/admin/factory.d.ts.map +1 -1
  171. package/build/ts/server/unauthenticated/admin/factory.js +2 -2
  172. package/build/ts/server/unauthenticated/admin/factory.js.map +1 -1
  173. package/build/ts/server/unauthenticated/admin/types.d.ts +76 -0
  174. package/build/ts/server/unauthenticated/admin/types.d.ts.map +1 -1
  175. package/build/ts/server/unauthenticated/admin/unauthenticated.admin.doc.d.ts.map +1 -1
  176. package/build/ts/server/unauthenticated/admin/unauthenticated.admin.doc.js +2 -5
  177. package/build/ts/server/unauthenticated/admin/unauthenticated.admin.doc.js.map +1 -1
  178. package/build/ts/server/unauthenticated/storefront/factory.d.ts.map +1 -1
  179. package/build/ts/server/unauthenticated/storefront/factory.js +2 -2
  180. package/build/ts/server/unauthenticated/storefront/factory.js.map +1 -1
  181. package/build/ts/server/unauthenticated/storefront/unauthenticated.storefront.doc.d.ts.map +1 -1
  182. package/build/ts/server/unauthenticated/storefront/unauthenticated.storefront.doc.js +1 -4
  183. package/build/ts/server/unauthenticated/storefront/unauthenticated.storefront.doc.js.map +1 -1
  184. package/build/ts/server/version.d.ts +1 -1
  185. package/build/ts/server/version.js +1 -1
  186. package/package.json +12 -16
package/README.md CHANGED
@@ -174,6 +174,50 @@ Here are some guides to help you set up your app:
174
174
  You can also authenticate requests from surfaces other than the admin.
175
175
  To see all supported methods, see [the `shopify.authenticate` object documentation](https://shopify.dev/docs/api/shopify-app-remix/latest/authenticate).
176
176
 
177
+ ### New embedded app authorization strategy
178
+
179
+ > [!TIP]
180
+ > If you are building an embedded app, we **strongly** recommend using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
181
+ > with [token exchange](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange) instead of the legacy authorization code grant flow.
182
+
183
+ We've introduced a new installation and authorization strategy for **embedded apps** that
184
+ eliminates the redirects that were previously necessary.
185
+ It replaces the existing [installation and authorization code grant flow](https://shopify.dev/docs/apps/auth/get-access-tokens/authorization-code-grant).
186
+
187
+ This is achieved by using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
188
+ to handle automatic app installations and scope updates, while utilizing
189
+ [token exchange](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange) to retrieve an access token for
190
+ authenticated API access.
191
+
192
+ ##### Enabling this new strategy in your app
193
+
194
+ > [!NOTE]
195
+ > Newly created Remix apps from the template after February 1st 2024 has this feature enabled by default.
196
+
197
+ 1. Enable [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
198
+ by configuring your scopes [through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration).
199
+ 2. Enable the future flag `unstable_newEmbeddedAuthStrategy` in your app's server configuration file.
200
+
201
+ ```ts
202
+ // my-app/app/shopify.server.ts
203
+ const shopify = shopifyApp({
204
+ ...
205
+ isEmbeddedApp: true,
206
+ future: {
207
+ unstable_newEmbeddedAuthStrategy: true,
208
+ }
209
+ })
210
+
211
+ ```
212
+
213
+ 3. Enjoy a smoother and faster app installation process.
214
+
215
+ ###### Learn more about:
216
+
217
+ - [How token exchange works](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange)
218
+ - [Using Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
219
+ - [Configuring access scopes through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration)
220
+
177
221
  ## Gotchas / Troubleshooting
178
222
 
179
223
  If you're experiencing unexpected behaviors when using this package, check our [app template's documentation](https://github.com/Shopify/shopify-app-template-remix#gotchas--troubleshooting) for the solution to common issues.
@@ -4,6 +4,7 @@ var crypto = require('crypto');
4
4
  var runtime = require('@shopify/shopify-api/runtime');
5
5
  var appBridgeUrl = require('../../authenticate/helpers/app-bridge-url.js');
6
6
  require('isbot');
7
+ require('@remix-run/server-runtime');
7
8
 
8
9
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
10
 
@@ -7,14 +7,15 @@ var validateSessionToken = require('../helpers/validate-session-token.js');
7
7
  var getSessionTokenHeader = require('../helpers/get-session-token-header.js');
8
8
  var rejectBotRequest = require('../helpers/reject-bot-request.js');
9
9
  var respondToOptionsRequest = require('../helpers/respond-to-options-request.js');
10
+ require('@remix-run/server-runtime');
10
11
  var cancel = require('./billing/cancel.js');
11
12
  var require$1 = require('./billing/require.js');
12
13
  var request = require('./billing/request.js');
14
+ var check = require('./billing/check.js');
13
15
  var createAdminApiContext = require('./helpers/create-admin-api-context.js');
14
16
  var ensureAppIsEmbeddedIfRequired = require('./helpers/ensure-app-is-embedded-if-required.js');
15
17
  var ensureSessionTokenSearchParamIfRequired = require('./helpers/ensure-session-token-search-param-if-required.js');
16
18
  require('@shopify/shopify-api');
17
- require('@remix-run/server-runtime');
18
19
  var redirect = require('./helpers/redirect.js');
19
20
  var renderAppBridge = require('./helpers/render-app-bridge.js');
20
21
  var validateShopAndHostParams = require('./helpers/validate-shop-and-host-params.js');
@@ -55,15 +56,12 @@ function authStrategyFactory({
55
56
  });
56
57
  }
57
58
  }
58
- function createContext(request$1, session, sessionToken) {
59
+ function createContext(request$1, session, authStrategy, sessionToken) {
59
60
  const context = {
60
- admin: createAdminApiContext.createAdminApiContext(request$1, session, {
61
- api,
62
- logger,
63
- config
64
- }),
61
+ admin: createAdminApiContext.createAdminApiContext(session, params, authStrategy.handleClientError(request$1)),
65
62
  billing: {
66
63
  require: require$1.requireBillingFactory(params, request$1, session),
64
+ check: check.checkBillingFactory(params, request$1, session),
67
65
  request: request.requestBillingFactory(params, request$1, session),
68
66
  cancel: cancel.cancelBillingFactory(params, request$1, session)
69
67
  },
@@ -99,18 +97,23 @@ function authStrategyFactory({
99
97
  const {
100
98
  payload,
101
99
  shop,
102
- sessionId
100
+ sessionId,
101
+ sessionToken
103
102
  } = await getSessionTokenContext(params, request);
104
103
  logger.debug('Loading session from storage', {
105
104
  sessionId
106
105
  });
107
106
  const existingSession = sessionId ? await config.sessionStorage.loadSession(sessionId) : undefined;
108
- const session = await strategy.authenticate(request, existingSession, shop);
107
+ const session = await strategy.authenticate(request, {
108
+ session: existingSession,
109
+ sessionToken,
110
+ shop
111
+ });
109
112
  logger.debug('Request is valid, loaded session from session token', {
110
113
  shop: session.shop,
111
114
  isOnline: session.isOnline
112
115
  });
113
- return createContext(request, session, payload);
116
+ return createContext(request, session, strategy, payload);
114
117
  } catch (errorOrResponse) {
115
118
  if (errorOrResponse instanceof Response) {
116
119
  ensureCorsHeaders.ensureCORSHeadersFactory(params, request)(errorOrResponse);
@@ -129,10 +132,10 @@ async function getSessionTokenContext(params, request) {
129
132
  const searchParamSessionToken = getSessionTokenHeader.getSessionTokenFromUrlParam(request);
130
133
  const sessionToken = headerSessionToken || searchParamSessionToken;
131
134
  logger.debug('Attempting to authenticate session token', {
132
- sessionToken: {
135
+ sessionToken: JSON.stringify({
133
136
  header: headerSessionToken,
134
137
  search: searchParamSessionToken
135
- }
138
+ })
136
139
  });
137
140
  if (config.isEmbeddedApp) {
138
141
  const payload = await validateSessionToken.validateSessionToken({
@@ -150,7 +153,8 @@ async function getSessionTokenContext(params, request) {
150
153
  return {
151
154
  shop,
152
155
  payload,
153
- sessionId
156
+ sessionId,
157
+ sessionToken
154
158
  };
155
159
  }
156
160
  const url = new URL(request.url);
@@ -162,7 +166,8 @@ async function getSessionTokenContext(params, request) {
162
166
  return {
163
167
  shop,
164
168
  sessionId,
165
- payload: undefined
169
+ payload: undefined,
170
+ sessionToken
166
171
  };
167
172
  }
168
173
 
@@ -3,8 +3,8 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var shopifyApi = require('@shopify/shopify-api');
6
- var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
7
6
  require('@remix-run/server-runtime');
7
+ var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
8
8
 
9
9
  function cancelBillingFactory(params, request, session) {
10
10
  return async function cancelBilling(options) {
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var shopifyApi = require('@shopify/shopify-api');
6
+ require('@remix-run/server-runtime');
7
+ var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
8
+
9
+ function checkBillingFactory(params, request, session) {
10
+ return async function checkBilling(options) {
11
+ const {
12
+ api,
13
+ logger
14
+ } = params;
15
+ logger.debug('Checking billing plans', {
16
+ shop: session.shop,
17
+ ...options
18
+ });
19
+ try {
20
+ return await api.billing.check({
21
+ session,
22
+ plans: options.plans,
23
+ isTest: options.isTest,
24
+ returnObject: true
25
+ });
26
+ } catch (error) {
27
+ if (error instanceof shopifyApi.HttpResponseError && error.response.code === 401) {
28
+ logger.debug('API token was invalid, redirecting to OAuth', {
29
+ shop: session.shop
30
+ });
31
+ throw await redirectToAuthPage.redirectToAuthPage(params, request, session.shop);
32
+ } else {
33
+ throw error;
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ exports.checkBillingFactory = checkBillingFactory;
@@ -3,8 +3,8 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var shopifyApi = require('@shopify/shopify-api');
6
- var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
7
6
  require('@remix-run/server-runtime');
7
+ var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
8
8
 
9
9
  function requireBillingFactory(params, request, session) {
10
10
  const {
@@ -3,15 +3,12 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var factory = require('../../../clients/admin/factory.js');
6
- var handleClientError = require('./handle-client-error.js');
7
6
 
8
- function createAdminApiContext(request, session, params) {
7
+ function createAdminApiContext(session, params, handleClientError) {
9
8
  return factory.adminClientFactory({
10
9
  session,
11
10
  params,
12
- handleClientError: handleClientError.handleClientErrorFactory({
13
- request
14
- })
11
+ handleClientError
15
12
  });
16
13
  }
17
14
 
@@ -3,10 +3,10 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var shopifyApi = require('@shopify/shopify-api');
6
- var redirectToAuthPage = require('./redirect-to-auth-page.js');
7
6
 
8
7
  function handleClientErrorFactory({
9
- request
8
+ request,
9
+ onError
10
10
  }) {
11
11
  return async function handleClientError({
12
12
  error,
@@ -22,8 +22,12 @@ function handleClientErrorFactory({
22
22
  statusText: error.response.statusText,
23
23
  body: JSON.stringify(error.response.body)
24
24
  });
25
- if (error.response.code === 401) {
26
- throw await redirectToAuthPage.redirectToAuthPage(params, request, session.shop);
25
+ if (onError) {
26
+ await onError({
27
+ request,
28
+ session,
29
+ error
30
+ });
27
31
  }
28
32
 
29
33
  // forward a minimal copy of the upstream HTTP response instead of an Error:
@@ -11,12 +11,14 @@ const redirectToBouncePage = (params, url) => {
11
11
 
12
12
  // Make sure we always point to the configured app URL so it also works behind reverse proxies (that alter the Host
13
13
  // header).
14
- url.searchParams.set('shopify-reload', `${config.appUrl}${url.pathname}${url.search}`);
14
+ const searchParams = url.searchParams;
15
+ searchParams.delete('id_token');
16
+ searchParams.set('shopify-reload', `${config.appUrl}${url.pathname}?${searchParams.toString()}`);
15
17
 
16
18
  // eslint-disable-next-line no-warning-comments
17
19
  // TODO Make sure this works on chrome without a tunnel (weird HTTPS redirect issue)
18
20
  // https://github.com/orgs/Shopify/projects/6899/views/1?pane=issue&itemId=28376650
19
- throw serverRuntime.redirect(`${config.auth.patchSessionTokenPath}${url.search}`);
21
+ throw serverRuntime.redirect(`${config.auth.patchSessionTokenPath}?${searchParams.toString()}`);
20
22
  };
21
23
 
22
24
  exports.redirectToBouncePage = redirectToBouncePage;
@@ -49,13 +49,9 @@ function isBounceRequest(request) {
49
49
  return Boolean(getSessionTokenHeader.getSessionTokenHeader(request)) && request.headers.has('X-Shopify-Bounce');
50
50
  }
51
51
  function isDataRequest(request) {
52
- const {
53
- searchParams
54
- } = new URL(request.url);
55
52
  const isGet = request.method === 'GET';
56
53
  const sessionTokenHeader = Boolean(getSessionTokenHeader.getSessionTokenHeader(request));
57
- const sessionTokenSearchParam = searchParams.has('id_token');
58
- return sessionTokenHeader && !sessionTokenSearchParam && !isBounceRequest(request) && (!isEmbeddedRequest(request) || !isGet);
54
+ return sessionTokenHeader && !isBounceRequest(request) && (!isEmbeddedRequest(request) || !isGet);
59
55
  }
60
56
  function isEmbeddedRequest(request) {
61
57
  const {
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var createAdminApiContext = require('./create-admin-api-context.js');
6
6
 
7
- async function triggerAfterAuthHook(params, session, request) {
7
+ async function triggerAfterAuthHook(params, session, request, authStrategy) {
8
8
  const {
9
9
  config,
10
10
  logger
@@ -13,7 +13,7 @@ async function triggerAfterAuthHook(params, session, request) {
13
13
  logger.info('Running afterAuth hook');
14
14
  await config.hooks.afterAuth({
15
15
  session,
16
- admin: createAdminApiContext.createAdminApiContext(request, session, params)
16
+ admin: createAdminApiContext.createAdminApiContext(session, params, authStrategy.handleClientError(request))
17
17
  });
18
18
  }
19
19
  }
@@ -4,9 +4,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var shopifyApi = require('@shopify/shopify-api');
6
6
  var beginAuth = require('../helpers/begin-auth.js');
7
- var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
8
7
  var redirectToShopifyOrAppRoot = require('../helpers/redirect-to-shopify-or-app-root.js');
9
8
  require('@remix-run/server-runtime');
9
+ var handleClientError = require('../helpers/handle-client-error.js');
10
+ var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
10
11
  var redirectWithExitiframe = require('../helpers/redirect-with-exitiframe.js');
11
12
  var getSessionTokenHeader = require('../../helpers/get-session-token-header.js');
12
13
  var triggerAfterAuthHook = require('../helpers/trigger-after-auth-hook.js');
@@ -52,12 +53,16 @@ class AuthCodeFlowStrategy {
52
53
  await this.ensureInstalledOnShop(request);
53
54
  }
54
55
  }
55
- async authenticate(request, session, shop) {
56
+ async authenticate(request, sessionContext) {
56
57
  const {
57
58
  api,
58
59
  config,
59
60
  logger
60
61
  } = this;
62
+ const {
63
+ shop,
64
+ session
65
+ } = sessionContext;
61
66
  if (!session) {
62
67
  logger.debug('No session found, redirecting to OAuth', {
63
68
  shop
@@ -82,6 +87,28 @@ class AuthCodeFlowStrategy {
82
87
  });
83
88
  return session;
84
89
  }
90
+ handleClientError(request) {
91
+ const {
92
+ api,
93
+ config,
94
+ logger
95
+ } = this;
96
+ return handleClientError.handleClientErrorFactory({
97
+ request,
98
+ onError: async ({
99
+ session,
100
+ error
101
+ }) => {
102
+ if (error.response.code === 401) {
103
+ throw await redirectToAuthPage.redirectToAuthPage({
104
+ api,
105
+ config,
106
+ logger
107
+ }, request, session.shop);
108
+ }
109
+ }
110
+ });
111
+ }
85
112
  async ensureInstalledOnShop(request) {
86
113
  const {
87
114
  api,
@@ -197,7 +224,7 @@ class AuthCodeFlowStrategy {
197
224
  api,
198
225
  config,
199
226
  logger
200
- }, session, request);
227
+ }, session, request, this);
201
228
  throw await redirectToShopifyOrAppRoot.redirectToShopifyOrAppRoot(request, {
202
229
  api,
203
230
  config,
@@ -293,7 +320,7 @@ class AuthCodeFlowStrategy {
293
320
  shop
294
321
  };
295
322
  if (error.response) {
296
- context.response = JSON.stringify(error.response);
323
+ context.response = JSON.stringify(error.body);
297
324
  }
298
325
  logger.error(`Unexpected error during session validation: ${error.message}`, context);
299
326
  throw new Response(undefined, {
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var shopifyApi = require('@shopify/shopify-api');
6
+ require('isbot');
7
+ var respondToInvalidSessionToken = require('../../helpers/respond-to-invalid-session-token.js');
8
+ require('@remix-run/server-runtime');
9
+ var handleClientError = require('../helpers/handle-client-error.js');
10
+ var triggerAfterAuthHook = require('../helpers/trigger-after-auth-hook.js');
11
+
12
+ class TokenExchangeStrategy {
13
+ constructor({
14
+ api,
15
+ config,
16
+ logger
17
+ }) {
18
+ this.api = void 0;
19
+ this.config = void 0;
20
+ this.logger = void 0;
21
+ this.api = api;
22
+ this.config = config;
23
+ this.logger = logger;
24
+ }
25
+ async respondToOAuthRequests(_request) {}
26
+ async authenticate(request, sessionContext) {
27
+ const {
28
+ api,
29
+ config,
30
+ logger
31
+ } = this;
32
+ const {
33
+ shop,
34
+ session,
35
+ sessionToken
36
+ } = sessionContext;
37
+ if (!sessionToken) throw new shopifyApi.InvalidJwtError();
38
+ if (!session || session.isExpired()) {
39
+ logger.info('No valid session found');
40
+ logger.info('Requesting offline access token');
41
+ const {
42
+ session: offlineSession
43
+ } = await this.exchangeToken({
44
+ request,
45
+ sessionToken,
46
+ shop,
47
+ requestedTokenType: shopifyApi.RequestedTokenType.OfflineAccessToken
48
+ });
49
+ await config.sessionStorage.storeSession(offlineSession);
50
+ let newSession = offlineSession;
51
+ if (config.useOnlineTokens) {
52
+ logger.info('Requesting online access token');
53
+ const {
54
+ session: onlineSession
55
+ } = await this.exchangeToken({
56
+ request,
57
+ sessionToken,
58
+ shop,
59
+ requestedTokenType: shopifyApi.RequestedTokenType.OnlineAccessToken
60
+ });
61
+ await config.sessionStorage.storeSession(onlineSession);
62
+ newSession = onlineSession;
63
+ }
64
+ try {
65
+ await this.handleAfterAuthHook({
66
+ api,
67
+ config,
68
+ logger
69
+ }, newSession, request, sessionToken);
70
+ } catch (error) {
71
+ throw new Response(undefined, {
72
+ status: 500,
73
+ statusText: 'Internal Server Error'
74
+ });
75
+ }
76
+ return newSession;
77
+ }
78
+ return session;
79
+ }
80
+ handleClientError(request) {
81
+ const {
82
+ api,
83
+ config,
84
+ logger
85
+ } = this;
86
+ return handleClientError.handleClientErrorFactory({
87
+ request,
88
+ onError: async ({
89
+ session,
90
+ error
91
+ }) => {
92
+ if (error.response.code === 401) {
93
+ config.sessionStorage.deleteSession(session.id);
94
+ respondToInvalidSessionToken.respondToInvalidSessionToken({
95
+ params: {
96
+ config,
97
+ api,
98
+ logger
99
+ },
100
+ request
101
+ });
102
+ }
103
+ }
104
+ });
105
+ }
106
+ async exchangeToken({
107
+ request,
108
+ shop,
109
+ sessionToken,
110
+ requestedTokenType
111
+ }) {
112
+ const {
113
+ api,
114
+ config,
115
+ logger
116
+ } = this;
117
+ try {
118
+ return await api.auth.tokenExchange({
119
+ sessionToken,
120
+ shop,
121
+ requestedTokenType
122
+ });
123
+ } catch (error) {
124
+ var _error$response$body;
125
+ if (error instanceof shopifyApi.InvalidJwtError || error instanceof shopifyApi.HttpResponseError && error.response.code === 400 && ((_error$response$body = error.response.body) === null || _error$response$body === void 0 ? void 0 : _error$response$body.error) === 'invalid_subject_token') {
126
+ throw respondToInvalidSessionToken.respondToInvalidSessionToken({
127
+ params: {
128
+ api,
129
+ config,
130
+ logger
131
+ },
132
+ request,
133
+ retryRequest: true
134
+ });
135
+ }
136
+ throw new Response(undefined, {
137
+ status: 500,
138
+ statusText: 'Internal Server Error'
139
+ });
140
+ }
141
+ }
142
+ async handleAfterAuthHook(params, session, request, sessionToken) {
143
+ const {
144
+ config
145
+ } = params;
146
+ await config.idempotentPromiseHandler.handlePromise({
147
+ promiseFunction: () => {
148
+ return triggerAfterAuthHook.triggerAfterAuthHook(params, session, request, this);
149
+ },
150
+ identifier: sessionToken
151
+ });
152
+ }
153
+ }
154
+
155
+ exports.TokenExchangeStrategy = TokenExchangeStrategy;
@@ -4,6 +4,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  const APP_BRIDGE_URL = 'https://cdn.shopify.com/shopifycloud/app-bridge.js';
6
6
  const REAUTH_URL_HEADER = 'X-Shopify-API-Request-Failure-Reauthorize-Url';
7
+ const RETRY_INVALID_SESSION_HEADER = {
8
+ 'X-Shopify-Retry-Invalid-Session-Request': '1'
9
+ };
7
10
 
8
11
  exports.APP_BRIDGE_URL = APP_BRIDGE_URL;
9
12
  exports.REAUTH_URL_HEADER = REAUTH_URL_HEADER;
13
+ exports.RETRY_INVALID_SESSION_HEADER = RETRY_INVALID_SESSION_HEADER;
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var factory = require('../../clients/admin/factory.js');
6
+
7
+ function authenticateFlowFactory(params) {
8
+ const {
9
+ api,
10
+ config,
11
+ logger
12
+ } = params;
13
+ return async function authenticate(request) {
14
+ logger.info('Authenticating flow request');
15
+ if (request.method !== 'POST') {
16
+ logger.debug('Received a non-POST request for flow. Only POST requests are allowed.', {
17
+ url: request.url,
18
+ method: request.method
19
+ });
20
+ throw new Response(undefined, {
21
+ status: 405,
22
+ statusText: 'Method not allowed'
23
+ });
24
+ }
25
+ const rawBody = await request.text();
26
+ const result = await api.flow.validate({
27
+ rawBody,
28
+ rawRequest: request
29
+ });
30
+ if (!result.valid) {
31
+ logger.error('Received an invalid flow request', {
32
+ reason: result.reason
33
+ });
34
+ throw new Response(undefined, {
35
+ status: 400,
36
+ statusText: 'Bad Request'
37
+ });
38
+ }
39
+ const payload = JSON.parse(rawBody);
40
+ logger.debug('Flow request is valid, looking for an offline session', {
41
+ shop: payload.shopify_domain
42
+ });
43
+ const sessionId = api.session.getOfflineId(payload.shopify_domain);
44
+ const session = await config.sessionStorage.loadSession(sessionId);
45
+ if (!session) {
46
+ logger.info('Flow request could not find session', {
47
+ shop: payload.shopify_domain
48
+ });
49
+ throw new Response(undefined, {
50
+ status: 400,
51
+ statusText: 'Bad Request'
52
+ });
53
+ }
54
+ logger.debug('Found a session for the flow request', {
55
+ shop: session.shop
56
+ });
57
+ return {
58
+ session,
59
+ payload,
60
+ admin: factory.adminClientFactory({
61
+ params,
62
+ session
63
+ })
64
+ };
65
+ };
66
+ }
67
+
68
+ exports.authenticateFlowFactory = authenticateFlowFactory;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ const IDENTIFIER_TTL_MS = 60000;
6
+ class IdempotentPromiseHandler {
7
+ constructor() {
8
+ this.identifiers = void 0;
9
+ this.identifiers = new Map();
10
+ }
11
+ async handlePromise({
12
+ promiseFunction,
13
+ identifier
14
+ }) {
15
+ try {
16
+ if (this.isPromiseRunnable(identifier)) {
17
+ await promiseFunction();
18
+ }
19
+ } finally {
20
+ this.clearStaleIdentifiers();
21
+ }
22
+ return Promise.resolve();
23
+ }
24
+ isPromiseRunnable(identifier) {
25
+ if (!this.identifiers.has(identifier)) {
26
+ this.identifiers.set(identifier, Date.now());
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ async clearStaleIdentifiers() {
32
+ this.identifiers.forEach((date, identifier, map) => {
33
+ if (Date.now() - date > IDENTIFIER_TTL_MS) {
34
+ map.delete(identifier);
35
+ }
36
+ });
37
+ }
38
+ }
39
+
40
+ exports.IdempotentPromiseHandler = IdempotentPromiseHandler;