@shopify/shopify-app-remix 4.1.1 → 4.2.1

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 (276) hide show
  1. package/dist/cjs/react/components/AppProvider/AppProvider.js.map +1 -1
  2. package/dist/cjs/react/components/AppProxyForm/AppProxyForm.js +4 -3
  3. package/dist/cjs/react/components/AppProxyForm/AppProxyForm.js.map +1 -1
  4. package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js +4 -3
  5. package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js.map +1 -1
  6. package/dist/cjs/react/components/AppProxyProvider/AppProxyProvider.js.map +1 -1
  7. package/dist/cjs/server/authenticate/admin/helpers/redirect.js +2 -2
  8. package/dist/cjs/server/authenticate/admin/helpers/redirect.js.map +1 -1
  9. package/dist/cjs/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
  10. package/dist/cjs/server/authenticate/admin/strategies/merchant-custom-app.js.map +1 -1
  11. package/dist/cjs/server/authenticate/admin/strategies/token-exchange.js.map +1 -1
  12. package/dist/cjs/server/authenticate/webhooks/authenticate.js +34 -10
  13. package/dist/cjs/server/authenticate/webhooks/authenticate.js.map +1 -1
  14. package/dist/cjs/server/future/flags.js.map +1 -1
  15. package/dist/cjs/server/shopify-app.js +1 -0
  16. package/dist/cjs/server/shopify-app.js.map +1 -1
  17. package/dist/cjs/server/version.js +1 -1
  18. package/dist/cjs/server/version.js.map +1 -1
  19. package/dist/esm/react/components/AppProvider/AppProvider.mjs.map +1 -1
  20. package/dist/esm/react/components/AppProxyForm/AppProxyForm.mjs +5 -4
  21. package/dist/esm/react/components/AppProxyForm/AppProxyForm.mjs.map +1 -1
  22. package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs +5 -4
  23. package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs.map +1 -1
  24. package/dist/esm/react/components/AppProxyProvider/AppProxyProvider.mjs.map +1 -1
  25. package/dist/esm/server/authenticate/admin/helpers/redirect.mjs +2 -2
  26. package/dist/esm/server/authenticate/admin/helpers/redirect.mjs.map +1 -1
  27. package/dist/esm/server/authenticate/admin/strategies/auth-code-flow.mjs.map +1 -1
  28. package/dist/esm/server/authenticate/admin/strategies/merchant-custom-app.mjs.map +1 -1
  29. package/dist/esm/server/authenticate/admin/strategies/token-exchange.mjs.map +1 -1
  30. package/dist/esm/server/authenticate/webhooks/authenticate.mjs +35 -11
  31. package/dist/esm/server/authenticate/webhooks/authenticate.mjs.map +1 -1
  32. package/dist/esm/server/future/flags.mjs.map +1 -1
  33. package/dist/esm/server/shopify-app.mjs +1 -0
  34. package/dist/esm/server/shopify-app.mjs.map +1 -1
  35. package/dist/esm/server/version.mjs +1 -1
  36. package/dist/esm/server/version.mjs.map +1 -1
  37. package/dist/ts/react/components/AppProvider/AppProvider.d.ts +5 -1
  38. package/dist/ts/react/components/AppProvider/AppProvider.d.ts.map +1 -1
  39. package/dist/ts/react/components/AppProxyForm/AppProxyForm.d.ts +2 -1
  40. package/dist/ts/react/components/AppProxyForm/AppProxyForm.d.ts.map +1 -1
  41. package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts +2 -1
  42. package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts.map +1 -1
  43. package/dist/ts/react/components/AppProxyProvider/AppProxyProvider.d.ts +5 -1
  44. package/dist/ts/react/components/AppProxyProvider/AppProxyProvider.d.ts.map +1 -1
  45. package/dist/ts/server/authenticate/admin/billing/types.d.ts +4 -0
  46. package/dist/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
  47. package/dist/ts/server/authenticate/admin/scope/types.d.ts +2 -1
  48. package/dist/ts/server/authenticate/admin/scope/types.d.ts.map +1 -1
  49. package/dist/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
  50. package/dist/ts/server/authenticate/admin/strategies/merchant-custom-app.d.ts.map +1 -1
  51. package/dist/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -1
  52. package/dist/ts/server/authenticate/admin/types.d.ts +10 -0
  53. package/dist/ts/server/authenticate/admin/types.d.ts.map +1 -1
  54. package/dist/ts/server/authenticate/flow/types.d.ts +4 -0
  55. package/dist/ts/server/authenticate/flow/types.d.ts.map +1 -1
  56. package/dist/ts/server/authenticate/fulfillment-service/types.d.ts +4 -0
  57. package/dist/ts/server/authenticate/fulfillment-service/types.d.ts.map +1 -1
  58. package/dist/ts/server/authenticate/public/appProxy/types.d.ts +4 -0
  59. package/dist/ts/server/authenticate/public/appProxy/types.d.ts.map +1 -1
  60. package/dist/ts/server/authenticate/public/checkout/types.d.ts +4 -0
  61. package/dist/ts/server/authenticate/public/checkout/types.d.ts.map +1 -1
  62. package/dist/ts/server/authenticate/public/customer-account/types.d.ts +4 -0
  63. package/dist/ts/server/authenticate/public/customer-account/types.d.ts.map +1 -1
  64. package/dist/ts/server/authenticate/public/extension/types.d.ts.map +1 -1
  65. package/dist/ts/server/authenticate/webhooks/authenticate.d.ts.map +1 -1
  66. package/dist/ts/server/authenticate/webhooks/types.d.ts +36 -3
  67. package/dist/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
  68. package/dist/ts/server/boundary/error.d.ts +1 -1
  69. package/dist/ts/server/boundary/error.d.ts.map +1 -1
  70. package/dist/ts/server/clients/admin/types.d.ts +4 -0
  71. package/dist/ts/server/clients/admin/types.d.ts.map +1 -1
  72. package/dist/ts/server/clients/storefront/types.d.ts +4 -0
  73. package/dist/ts/server/clients/storefront/types.d.ts.map +1 -1
  74. package/dist/ts/server/config-types.d.ts.map +1 -1
  75. package/dist/ts/server/future/flags.d.ts +6 -0
  76. package/dist/ts/server/future/flags.d.ts.map +1 -1
  77. package/dist/ts/server/shopify-app.d.ts +1 -0
  78. package/dist/ts/server/shopify-app.d.ts.map +1 -1
  79. package/dist/ts/server/unauthenticated/admin/types.d.ts +4 -0
  80. package/dist/ts/server/unauthenticated/admin/types.d.ts.map +1 -1
  81. package/dist/ts/server/unauthenticated/storefront/types.d.ts +4 -0
  82. package/dist/ts/server/unauthenticated/storefront/types.d.ts.map +1 -1
  83. package/dist/ts/server/version.d.ts +1 -1
  84. package/dist/tsconfig.build.tsbuildinfo +1 -1
  85. package/package.json +19 -15
  86. package/src/react/components/AppProvider/AppProvider.tsx +151 -0
  87. package/src/react/components/AppProvider/index.ts +1 -0
  88. package/src/react/components/AppProxyForm/AppProxyForm.tsx +85 -0
  89. package/src/react/components/AppProxyForm/index.ts +1 -0
  90. package/src/react/components/AppProxyLink/AppProxyLink.tsx +61 -0
  91. package/src/react/components/AppProxyLink/index.ts +1 -0
  92. package/src/react/components/AppProxyProvider/AppProxyProvider.tsx +104 -0
  93. package/src/react/components/AppProxyProvider/index.ts +5 -0
  94. package/src/react/components/RemixPolarisLink.tsx +14 -0
  95. package/src/react/components/index.ts +4 -0
  96. package/src/react/const.ts +2 -0
  97. package/src/react/index.ts +1 -0
  98. package/src/server/__test-helpers/const.ts +14 -0
  99. package/src/server/__test-helpers/expect-admin-api-client.ts +51 -0
  100. package/src/server/__test-helpers/expect-begin-auth-redirect.ts +22 -0
  101. package/src/server/__test-helpers/expect-document-request-headers.ts +25 -0
  102. package/src/server/__test-helpers/expect-exit-iframe.ts +37 -0
  103. package/src/server/__test-helpers/expect-login-redirect.ts +8 -0
  104. package/src/server/__test-helpers/expect-storefront-api-client.ts +49 -0
  105. package/src/server/__test-helpers/expect-token-refresh.ts +188 -0
  106. package/src/server/__test-helpers/get-hmac.ts +7 -0
  107. package/src/server/__test-helpers/get-jwt.ts +10 -0
  108. package/src/server/__test-helpers/get-thrown-error.ts +19 -0
  109. package/src/server/__test-helpers/get-thrown-response.ts +17 -0
  110. package/src/server/__test-helpers/index.ts +21 -0
  111. package/src/server/__test-helpers/mock-graphql-request.ts +60 -0
  112. package/src/server/__test-helpers/request-mock.ts +169 -0
  113. package/src/server/__test-helpers/setup-embedded-flow.ts +30 -0
  114. package/src/server/__test-helpers/setup-fetch-flow.ts +29 -0
  115. package/src/server/__test-helpers/setup-non-embedded-flow.ts +31 -0
  116. package/src/server/__test-helpers/setup-valid-request.ts +78 -0
  117. package/src/server/__test-helpers/setup-valid-session.ts +28 -0
  118. package/src/server/__test-helpers/sign-request-cookie.ts +32 -0
  119. package/src/server/__test-helpers/test-config.ts +56 -0
  120. package/src/server/adapters/node/index.ts +13 -0
  121. package/src/server/adapters/vercel/index.ts +13 -0
  122. package/src/server/authenticate/admin/authenticate.admin.doc.example.ts +34 -0
  123. package/src/server/authenticate/admin/authenticate.ts +232 -0
  124. package/src/server/authenticate/admin/billing/cancel.ts +38 -0
  125. package/src/server/authenticate/admin/billing/check.ts +41 -0
  126. package/src/server/authenticate/admin/billing/create-usage-record.ts +36 -0
  127. package/src/server/authenticate/admin/billing/helpers.ts +42 -0
  128. package/src/server/authenticate/admin/billing/index.ts +6 -0
  129. package/src/server/authenticate/admin/billing/request.ts +64 -0
  130. package/src/server/authenticate/admin/billing/require.ts +58 -0
  131. package/src/server/authenticate/admin/billing/types.ts +617 -0
  132. package/src/server/authenticate/admin/billing/update-usage-subscription-capped-amount.ts +55 -0
  133. package/src/server/authenticate/admin/helpers/begin-auth.ts +17 -0
  134. package/src/server/authenticate/admin/helpers/create-admin-api-context.ts +20 -0
  135. package/src/server/authenticate/admin/helpers/ensure-app-is-embedded-if-required.ts +18 -0
  136. package/src/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.ts +25 -0
  137. package/src/server/authenticate/admin/helpers/handle-client-error.ts +45 -0
  138. package/src/server/authenticate/admin/helpers/index.ts +14 -0
  139. package/src/server/authenticate/admin/helpers/redirect-to-auth-page.ts +28 -0
  140. package/src/server/authenticate/admin/helpers/redirect-to-bounce-page.ts +23 -0
  141. package/src/server/authenticate/admin/helpers/redirect-to-install-page.ts +53 -0
  142. package/src/server/authenticate/admin/helpers/redirect-to-shopify-or-app-root.ts +21 -0
  143. package/src/server/authenticate/admin/helpers/redirect-with-app-bridge-headers.ts +13 -0
  144. package/src/server/authenticate/admin/helpers/redirect-with-exitiframe.ts +28 -0
  145. package/src/server/authenticate/admin/helpers/redirect.ts +164 -0
  146. package/src/server/authenticate/admin/helpers/render-app-bridge.ts +46 -0
  147. package/src/server/authenticate/admin/helpers/trigger-after-auth-hook.ts +29 -0
  148. package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +67 -0
  149. package/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +46 -0
  150. package/src/server/authenticate/admin/scope/client/fetch-scopes-details.ts +43 -0
  151. package/src/server/authenticate/admin/scope/client/revoke-scopes.ts +38 -0
  152. package/src/server/authenticate/admin/scope/factory.ts +21 -0
  153. package/src/server/authenticate/admin/scope/query.ts +51 -0
  154. package/src/server/authenticate/admin/scope/request.ts +32 -0
  155. package/src/server/authenticate/admin/scope/revoke.ts +48 -0
  156. package/src/server/authenticate/admin/scope/types.ts +127 -0
  157. package/src/server/authenticate/admin/strategies/auth-code-flow.ts +332 -0
  158. package/src/server/authenticate/admin/strategies/merchant-custom-app.ts +65 -0
  159. package/src/server/authenticate/admin/strategies/token-exchange.ts +188 -0
  160. package/src/server/authenticate/admin/strategies/types.ts +29 -0
  161. package/src/server/authenticate/admin/types.ts +226 -0
  162. package/src/server/authenticate/const.ts +15 -0
  163. package/src/server/authenticate/flow/authenticate.flow.doc.example.ts +39 -0
  164. package/src/server/authenticate/flow/authenticate.ts +68 -0
  165. package/src/server/authenticate/flow/types.ts +89 -0
  166. package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts +52 -0
  167. package/src/server/authenticate/fulfillment-service/authenticate.ts +82 -0
  168. package/src/server/authenticate/fulfillment-service/types.ts +86 -0
  169. package/src/server/authenticate/helpers/add-response-headers.ts +43 -0
  170. package/src/server/authenticate/helpers/app-bridge-url.ts +10 -0
  171. package/src/server/authenticate/helpers/ensure-cors-headers.ts +38 -0
  172. package/src/server/authenticate/helpers/get-session-token-header.ts +11 -0
  173. package/src/server/authenticate/helpers/get-shop-from-request.ts +4 -0
  174. package/src/server/authenticate/helpers/idempotent-promise-handler.ts +45 -0
  175. package/src/server/authenticate/helpers/index.ts +10 -0
  176. package/src/server/authenticate/helpers/invalidate-access-token.ts +17 -0
  177. package/src/server/authenticate/helpers/reject-bot-request.ts +27 -0
  178. package/src/server/authenticate/helpers/respond-to-invalid-session-token.ts +28 -0
  179. package/src/server/authenticate/helpers/respond-to-options-request.ts +26 -0
  180. package/src/server/authenticate/helpers/validate-session-token.ts +40 -0
  181. package/src/server/authenticate/login/login.ts +53 -0
  182. package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.example.ts +27 -0
  183. package/src/server/authenticate/public/appProxy/authenticate.ts +150 -0
  184. package/src/server/authenticate/public/appProxy/types.ts +219 -0
  185. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.offers.ts +21 -0
  186. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.ts +17 -0
  187. package/src/server/authenticate/public/checkout/authenticate.ts +13 -0
  188. package/src/server/authenticate/public/checkout/types.ts +69 -0
  189. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.offers.ts +36 -0
  190. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.ts +18 -0
  191. package/src/server/authenticate/public/customer-account/authenticate.ts +13 -0
  192. package/src/server/authenticate/public/customer-account/types.ts +69 -0
  193. package/src/server/authenticate/public/extension/authenticate.ts +54 -0
  194. package/src/server/authenticate/public/extension/types.ts +11 -0
  195. package/src/server/authenticate/public/factory.ts +24 -0
  196. package/src/server/authenticate/public/index.ts +1 -0
  197. package/src/server/authenticate/public/pos/authenticate.ts +8 -0
  198. package/src/server/authenticate/public/pos/types.ts +65 -0
  199. package/src/server/authenticate/public/types.ts +96 -0
  200. package/src/server/authenticate/webhooks/authenticate.ts +105 -0
  201. package/src/server/authenticate/webhooks/authenticate.webhooks.doc.example.ts +43 -0
  202. package/src/server/authenticate/webhooks/index.ts +1 -0
  203. package/src/server/authenticate/webhooks/register.ts +48 -0
  204. package/src/server/authenticate/webhooks/types.ts +267 -0
  205. package/src/server/boundary/error.tsx +14 -0
  206. package/src/server/boundary/headers.ts +15 -0
  207. package/src/server/boundary/index.ts +39 -0
  208. package/src/server/boundary/types.ts +4 -0
  209. package/src/server/clients/admin/factory.ts +22 -0
  210. package/src/server/clients/admin/graphql.ts +39 -0
  211. package/src/server/clients/admin/index.ts +2 -0
  212. package/src/server/clients/admin/types.ts +124 -0
  213. package/src/server/clients/index.ts +2 -0
  214. package/src/server/clients/storefront/factory.ts +32 -0
  215. package/src/server/clients/storefront/index.ts +2 -0
  216. package/src/server/clients/storefront/types.ts +82 -0
  217. package/src/server/clients/types.ts +47 -0
  218. package/src/server/config-types.ts +309 -0
  219. package/src/server/errors.ts +3 -0
  220. package/src/server/future/flags.ts +68 -0
  221. package/src/server/helpers/create-or-load-offline-session.ts +19 -0
  222. package/src/server/helpers/ensure-offline-token-is-not-expired.ts +32 -0
  223. package/src/server/helpers/ensure-valid-offline-session.ts +15 -0
  224. package/src/server/helpers/index.ts +4 -0
  225. package/src/server/helpers/refresh-token.ts +35 -0
  226. package/src/server/index.ts +24 -0
  227. package/src/server/override-logger.ts +42 -0
  228. package/src/server/shopify-app.ts +216 -0
  229. package/src/server/test-helpers/index.ts +1 -0
  230. package/src/server/test-helpers/test-config.ts +79 -0
  231. package/src/server/types-contexts.ts +44 -0
  232. package/src/server/types.ts +507 -0
  233. package/src/server/unauthenticated/admin/factory.ts +23 -0
  234. package/src/server/unauthenticated/admin/index.ts +1 -0
  235. package/src/server/unauthenticated/admin/types.ts +83 -0
  236. package/src/server/unauthenticated/storefront/factory.ts +28 -0
  237. package/src/server/unauthenticated/storefront/index.ts +1 -0
  238. package/src/server/unauthenticated/storefront/types.ts +109 -0
  239. package/src/server/unauthenticated/types.ts +65 -0
  240. package/src/server/version.ts +1 -0
  241. package/dist/ts/react/components/AppProvider/AppProvider.doc.d.ts +0 -4
  242. package/dist/ts/react/components/AppProvider/AppProvider.doc.d.ts.map +0 -1
  243. package/dist/ts/react/components/AppProxyForm/AppProxyForm.doc.d.ts +0 -4
  244. package/dist/ts/react/components/AppProxyForm/AppProxyForm.doc.d.ts.map +0 -1
  245. package/dist/ts/react/components/AppProxyLink/AppProxyLink.doc.d.ts +0 -4
  246. package/dist/ts/react/components/AppProxyLink/AppProxyLink.doc.d.ts.map +0 -1
  247. package/dist/ts/react/components/AppProxyProvider/AppProxyProvider.doc.d.ts +0 -4
  248. package/dist/ts/react/components/AppProxyProvider/AppProxyProvider.doc.d.ts.map +0 -1
  249. package/dist/ts/server/authenticate/admin/authenticate.admin.doc.d.ts +0 -4
  250. package/dist/ts/server/authenticate/admin/authenticate.admin.doc.d.ts.map +0 -1
  251. package/dist/ts/server/authenticate/admin/billing/authenticate.admin.billing.doc.d.ts +0 -4
  252. package/dist/ts/server/authenticate/admin/billing/authenticate.admin.billing.doc.d.ts.map +0 -1
  253. package/dist/ts/server/authenticate/admin/scope/authenticate.admin.scopes.doc.d.ts +0 -4
  254. package/dist/ts/server/authenticate/admin/scope/authenticate.admin.scopes.doc.d.ts.map +0 -1
  255. package/dist/ts/server/authenticate/flow/authenticate.flow.doc.d.ts +0 -4
  256. package/dist/ts/server/authenticate/flow/authenticate.flow.doc.d.ts.map +0 -1
  257. package/dist/ts/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.d.ts +0 -4
  258. package/dist/ts/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.d.ts.map +0 -1
  259. package/dist/ts/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.d.ts +0 -4
  260. package/dist/ts/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.d.ts.map +0 -1
  261. package/dist/ts/server/authenticate/public/checkout/authenticate.public.checkout.doc.d.ts +0 -4
  262. package/dist/ts/server/authenticate/public/checkout/authenticate.public.checkout.doc.d.ts.map +0 -1
  263. package/dist/ts/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.d.ts +0 -4
  264. package/dist/ts/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.d.ts.map +0 -1
  265. package/dist/ts/server/authenticate/webhooks/authenticate.webhooks.doc.d.ts +0 -4
  266. package/dist/ts/server/authenticate/webhooks/authenticate.webhooks.doc.d.ts.map +0 -1
  267. package/dist/ts/server/clients/admin/authenticate.admin.api.doc.d.ts +0 -4
  268. package/dist/ts/server/clients/admin/authenticate.admin.api.doc.d.ts.map +0 -1
  269. package/dist/ts/server/clients/storefront/authenticate.storefront.api.doc.d.ts +0 -4
  270. package/dist/ts/server/clients/storefront/authenticate.storefront.api.doc.d.ts.map +0 -1
  271. package/dist/ts/server/shopify-app.doc.d.ts +0 -4
  272. package/dist/ts/server/shopify-app.doc.d.ts.map +0 -1
  273. package/dist/ts/server/unauthenticated/admin/unauthenticated.admin.doc.d.ts +0 -4
  274. package/dist/ts/server/unauthenticated/admin/unauthenticated.admin.doc.d.ts.map +0 -1
  275. package/dist/ts/server/unauthenticated/storefront/unauthenticated.storefront.doc.d.ts +0 -4
  276. package/dist/ts/server/unauthenticated/storefront/unauthenticated.storefront.doc.d.ts.map +0 -1
@@ -0,0 +1,38 @@
1
+ import {AdminApiContext} from '../../../../clients';
2
+
3
+ export interface RevokeScopesResponse {
4
+ revoked: {
5
+ handle: string;
6
+ }[];
7
+ userErrors: {
8
+ field: string;
9
+ message: string;
10
+ }[];
11
+ }
12
+
13
+ const REVOKE_SCOPE_MUTATION = `#graphql
14
+ mutation AppRevokeAccessScopes($scopes: [String!]!) {
15
+ appRevokeAccessScopes(scopes: $scopes){
16
+ revoked {
17
+ handle
18
+ }
19
+ userErrors {
20
+ field
21
+ message
22
+ }
23
+ }
24
+ }`;
25
+
26
+ export async function revokeScopes(
27
+ admin: AdminApiContext,
28
+ scopes: string[],
29
+ ): Promise<RevokeScopesResponse> {
30
+ const revokeScopesResult = await admin.graphql(REVOKE_SCOPE_MUTATION, {
31
+ variables: {
32
+ scopes,
33
+ },
34
+ });
35
+
36
+ const resultContent = await revokeScopesResult.json();
37
+ return resultContent.data.appRevokeAccessScopes;
38
+ }
@@ -0,0 +1,21 @@
1
+ import {Session} from '@shopify/shopify-api';
2
+
3
+ import {BasicParams} from '../../../types';
4
+ import {AdminApiContext} from '../../../clients';
5
+
6
+ import {ScopesApiContext} from './types';
7
+ import {requestScopesFactory} from './request';
8
+ import {queryScopesFactory} from './query';
9
+ import {revokeScopesFactory} from './revoke';
10
+
11
+ export function scopesApiFactory(
12
+ params: BasicParams,
13
+ session: Session,
14
+ admin: AdminApiContext,
15
+ ): ScopesApiContext {
16
+ return {
17
+ query: queryScopesFactory(params, session, admin),
18
+ request: requestScopesFactory(params, session, admin),
19
+ revoke: revokeScopesFactory(params, session, admin),
20
+ };
21
+ }
@@ -0,0 +1,51 @@
1
+ import {AuthScopes, Session} from '@shopify/shopify-api';
2
+
3
+ import {AdminApiContext} from '../../../clients';
4
+ import type {BasicParams} from '../../../types';
5
+
6
+ import {ScopesDetail} from './types';
7
+ import {
8
+ FetchScopesDetailResponse,
9
+ fetchScopeDetail,
10
+ } from './client/fetch-scopes-details';
11
+
12
+ export function queryScopesFactory(
13
+ params: BasicParams,
14
+ session: Session,
15
+ admin: AdminApiContext,
16
+ ) {
17
+ return async function queryScopes() {
18
+ const {logger} = params;
19
+
20
+ logger.debug('Querying scopes details: ', {
21
+ shop: session.shop,
22
+ });
23
+
24
+ const scopesDetail = await fetchScopeDetail(admin);
25
+ return mapFetchScopeDetail(scopesDetail);
26
+ };
27
+ }
28
+
29
+ export function mapFetchScopeDetail(
30
+ scopesDetailResponse: FetchScopesDetailResponse,
31
+ ): ScopesDetail {
32
+ const appInformation = scopesDetailResponse.app;
33
+
34
+ const granted = new AuthScopes(
35
+ appInformation.installation.accessScopes.map((scope) => scope.handle),
36
+ ).toArray(true);
37
+
38
+ const required = new AuthScopes(
39
+ appInformation.requestedAccessScopes.map((scope) => scope.handle),
40
+ ).toArray(true);
41
+
42
+ const optional = new AuthScopes(
43
+ appInformation.optionalAccessScopes.map((scope) => scope.handle),
44
+ ).toArray(true);
45
+
46
+ return {
47
+ granted,
48
+ required,
49
+ optional,
50
+ };
51
+ }
@@ -0,0 +1,32 @@
1
+ import {AuthScopes, Session} from '@shopify/shopify-api';
2
+
3
+ import type {BasicParams} from '../../../types';
4
+ import {redirectToInstallPage} from '../helpers/redirect-to-install-page';
5
+ import {AdminApiContext} from '../../../clients';
6
+
7
+ import {fetchScopeDetail} from './client/fetch-scopes-details';
8
+
9
+ export function requestScopesFactory(
10
+ params: BasicParams,
11
+ session: Session,
12
+ admin: AdminApiContext,
13
+ ) {
14
+ return async function requestScopes(scopes: string[]) {
15
+ const {logger} = params;
16
+
17
+ logger.debug('Requesting optional scopes: ', {shop: session.shop, scopes});
18
+
19
+ if (scopes.length === 0) return;
20
+ if (await alreadyGranted(scopes, admin)) return;
21
+
22
+ throw await redirectToInstallPage(params, session.shop, scopes);
23
+ };
24
+
25
+ async function alreadyGranted(scopes: string[], admin: AdminApiContext) {
26
+ const scopesDetail = await fetchScopeDetail(admin);
27
+ const grantedScopes = scopesDetail.app.installation.accessScopes.map(
28
+ (scope) => scope.handle,
29
+ );
30
+ return new AuthScopes(grantedScopes).has(scopes);
31
+ }
32
+ }
@@ -0,0 +1,48 @@
1
+ import {Session} from '@shopify/shopify-api';
2
+
3
+ import {AdminApiContext} from '../../../clients';
4
+ import type {BasicParams} from '../../../types';
5
+
6
+ import {revokeScopes} from './client/revoke-scopes';
7
+
8
+ export function revokeScopesFactory(
9
+ params: BasicParams,
10
+ session: Session,
11
+ admin: AdminApiContext,
12
+ ) {
13
+ return async function revoke(scopes: string[]) {
14
+ const {logger} = params;
15
+
16
+ await validateScopes(scopes);
17
+
18
+ logger.debug('Revoke scopes: ', {
19
+ shop: session.shop,
20
+ scopes,
21
+ });
22
+
23
+ const revokeScopesResult = await revokeScopes(admin, scopes);
24
+ if (revokeScopesResult.userErrors?.length > 0) {
25
+ logger.error('Failed to revoke scopes: ', {
26
+ shop: session.shop,
27
+ errors: revokeScopesResult.userErrors,
28
+ });
29
+
30
+ throw new Response(JSON.stringify(revokeScopesResult.userErrors), {
31
+ status: 422,
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ });
36
+ }
37
+
38
+ return {
39
+ revoked: revokeScopesResult.revoked.map((scope) => scope.handle),
40
+ };
41
+ };
42
+ }
43
+
44
+ async function validateScopes(scopes: string[]) {
45
+ if (!scopes || scopes.length === 0) {
46
+ throw new Response('No scopes provided', {status: 400});
47
+ }
48
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Provides utilities that apps can use to [manage scopes](https://shopify.dev/docs/apps/build/authentication-authorization/app-installation/manage-access-scopes) for the app using the Admin API.
3
+ * @publicDocs
4
+ */
5
+ export interface ScopesApiContext {
6
+ /**
7
+ * Queries Shopify to see what scopes have been granted
8
+ *
9
+ * @returns {ScopesDetail} The scope details.
10
+ *
11
+ * @example
12
+ * <caption>Query for granted scopes.</caption>
13
+ * <description>Call `scopes.query` to get scope details.</description>
14
+ * ```ts
15
+ * // /app._index.tsx
16
+ * import { authenticate } from "../shopify.server";
17
+ * import { json } from "@remix-run/node";
18
+ *
19
+ * export const loader = async ({ request }) => {
20
+ * const { scopes } = await authenticate.admin(request);
21
+ * const { required, optional, granted } = await scopes.query();
22
+ *
23
+ * if (granted.includes("write_products")) {
24
+ * // do something
25
+ * }
26
+ *
27
+ * return null
28
+ * };
29
+ *
30
+ * ```
31
+ */
32
+ query: () => Promise<ScopesDetail>;
33
+
34
+ /**
35
+ * Requests the merchant grant the provided scopes
36
+ *
37
+ * This method performs a redirect to the grant screen.
38
+ *
39
+ * @example
40
+ * <caption>Ask the merchant to grant the provided scopes.</caption>
41
+ * <description>Call `scopes.request` to request optional scopes.</description>
42
+ * ```ts
43
+ * // /app/routes/app.request.tsx
44
+ * import { authenticate } from "../shopify.server";
45
+ *
46
+ * // Example of an action to request optional scopes
47
+ * export const action = async ({ request }) => {
48
+ * const { scopes } = await authenticate.admin(request);
49
+ * const { granted } = await scopes.query();
50
+ *
51
+ * if (!granted.includes("write_products")) {
52
+ * await scopes.request(["write_products"]);
53
+ * }
54
+ *
55
+ * return null
56
+ * };
57
+ * ```
58
+ */
59
+ request: (scopes: Scope[]) => Promise<void>;
60
+
61
+ /**
62
+ * Revokes the provided scopes
63
+ *
64
+ * Warning: This method throws an [error](https://shopify.dev/docs/api/admin-graphql/unstable/objects/AppRevokeAccessScopesAppRevokeScopeError) if the provided optional scopes contains a required scope.
65
+ *
66
+ * @example
67
+ * <caption>Revoke optional scopes.</caption>
68
+ * <description>Call `scopes.revoke` to revoke optional scopes.</description>
69
+ * ```ts
70
+ * // /app._index.tsx
71
+ * import { authenticate } from "../shopify.server";
72
+ * import { json } from "@remix-run/node";
73
+ *
74
+ * // Example of an action to POST optional scopes to revoke
75
+ * export const action = async ({ request }) => {
76
+ * const { scopes } = await authenticate.admin(request);
77
+ * const { granted } = await scopes.query();
78
+ *
79
+ * if (granted.includes("write_products")) {
80
+ * const revokedResponse = await scopes.revoke(["write_products"]);
81
+ * return json(revokedResponse);
82
+ * }
83
+ *
84
+ * return null
85
+ * };
86
+ *
87
+ * ```
88
+ */
89
+ revoke: (scopes: Scope[]) => Promise<ScopesRevokeResponse>;
90
+ }
91
+
92
+ /**
93
+ * `Scope` represents an [access scope](https://shopify.dev/docs/api/usage/access-scopes) handle, like "`write_products`" or "`read_orders`"
94
+ */
95
+ export type Scope = string;
96
+
97
+ /**
98
+ * `UserResult` represents the results of a user responding to a scopes request, i.e. a merchant user’s action taken when presented with a grant modal.
99
+ */
100
+ export type UserResult = 'granted-all' | 'declined-all';
101
+
102
+ export interface ScopesDetail {
103
+ /**
104
+ * The scopes that have been granted on the shop for this app
105
+ */
106
+ granted: Scope[];
107
+ /**
108
+ * The required scopes that the app has declared in its configuration
109
+ */
110
+ required: Scope[];
111
+ /**
112
+ * The optional scopes that the app has declared in its configuration
113
+ */
114
+ optional: Scope[];
115
+ }
116
+
117
+ export interface ScopesRequestResponse {
118
+ result: UserResult;
119
+ detail: ScopesDetail;
120
+ }
121
+
122
+ export interface ScopesRevokeResponse {
123
+ /**
124
+ * The scopes that have been revoked on the shop for this app
125
+ */
126
+ revoked: Scope[];
127
+ }
@@ -0,0 +1,332 @@
1
+ import {
2
+ CookieNotFound,
3
+ GraphqlQueryError,
4
+ HttpResponseError,
5
+ InvalidHmacError,
6
+ InvalidOAuthError,
7
+ Session,
8
+ Shopify,
9
+ } from '@shopify/shopify-api';
10
+
11
+ import type {BasicParams} from '../../../types';
12
+ import {
13
+ beginAuth,
14
+ handleClientErrorFactory,
15
+ redirectToAuthPage,
16
+ redirectToShopifyOrAppRoot,
17
+ redirectWithExitIframe,
18
+ triggerAfterAuthHook,
19
+ validateShopAndHostParams,
20
+ } from '../helpers';
21
+ import {AppConfig, AppConfigArg} from '../../../config-types';
22
+ import {getSessionTokenHeader} from '../../helpers';
23
+ import {HandleAdminClientError} from '../../../clients';
24
+ import type {
25
+ ApiConfigWithFutureFlags,
26
+ ApiFutureFlags,
27
+ } from '../../../future/flags';
28
+
29
+ import {AuthorizationStrategy, SessionContext, OnErrorOptions} from './types';
30
+
31
+ export class AuthCodeFlowStrategy<
32
+ Config extends AppConfigArg,
33
+ > implements AuthorizationStrategy {
34
+ protected api: Shopify<
35
+ ApiConfigWithFutureFlags<Config['future']>,
36
+ ApiFutureFlags<Config['future']>
37
+ >;
38
+
39
+ protected config: AppConfig;
40
+ protected logger: Shopify['logger'];
41
+
42
+ public constructor({api, config, logger}: BasicParams) {
43
+ this.api = api;
44
+ this.config = config;
45
+ this.logger = logger;
46
+ }
47
+
48
+ public async respondToOAuthRequests(request: Request): Promise<void | never> {
49
+ const {api, config} = this;
50
+
51
+ const url = new URL(request.url);
52
+ const isAuthRequest = url.pathname === config.auth.path;
53
+ const isAuthCallbackRequest = url.pathname === config.auth.callbackPath;
54
+
55
+ if (isAuthRequest || isAuthCallbackRequest) {
56
+ const shop = api.utils.sanitizeShop(url.searchParams.get('shop')!);
57
+ if (!shop) throw new Response('Shop param is invalid', {status: 400});
58
+
59
+ if (isAuthRequest) {
60
+ throw await this.handleAuthBeginRequest(request, shop);
61
+ } else {
62
+ throw await this.handleAuthCallbackRequest(request, shop);
63
+ }
64
+ }
65
+
66
+ if (!getSessionTokenHeader(request)) {
67
+ // This is a document request that doesn't contain a session token. We check if the app is installed.
68
+ // If the app isn't installed, we initiate the OAuth auth code flow.
69
+ // Requests with a header can only happen after the app is installed.
70
+ await this.ensureInstalledOnShop(request);
71
+ }
72
+ }
73
+
74
+ public async authenticate(
75
+ request: Request,
76
+ sessionContext: SessionContext,
77
+ ): Promise<Session | never> {
78
+ const {api, config, logger} = this;
79
+
80
+ const {shop, session} = sessionContext;
81
+
82
+ if (!session) {
83
+ logger.debug('No session found, redirecting to OAuth', {shop});
84
+ await redirectToAuthPage({config, logger, api}, request, shop);
85
+ } else if (!session.isActive(config.scopes)) {
86
+ logger.debug(
87
+ 'Found a session, but it has expired, redirecting to OAuth',
88
+ {shop},
89
+ );
90
+ await redirectToAuthPage({config, logger, api}, request, shop);
91
+ }
92
+
93
+ logger.debug('Found a valid session', {shop});
94
+
95
+ return session!;
96
+ }
97
+
98
+ public handleClientError(request: Request): HandleAdminClientError {
99
+ const {api, config, logger} = this;
100
+ return handleClientErrorFactory({
101
+ request,
102
+ onError: async ({session, error}: OnErrorOptions) => {
103
+ if (error.response.code === 401) {
104
+ throw await redirectToAuthPage(
105
+ {api, config, logger},
106
+ request,
107
+ session.shop,
108
+ );
109
+ }
110
+ },
111
+ });
112
+ }
113
+
114
+ private async ensureInstalledOnShop(request: Request) {
115
+ const {api, config, logger} = this;
116
+
117
+ validateShopAndHostParams({api, config, logger}, request);
118
+
119
+ const url = new URL(request.url);
120
+ let shop = url.searchParams.get('shop');
121
+
122
+ // Ensure app is installed
123
+ logger.debug('Ensuring app is installed on shop', {shop});
124
+
125
+ if (!(await this.hasValidOfflineId(request))) {
126
+ logger.info("Could not find a shop, can't authenticate request");
127
+ throw new Response(undefined, {
128
+ status: 400,
129
+ statusText: 'Bad Request',
130
+ });
131
+ }
132
+
133
+ const offlineSession = await this.getOfflineSession(request);
134
+ const isEmbedded = url.searchParams.get('embedded') === '1';
135
+
136
+ if (!offlineSession) {
137
+ logger.info("Shop hasn't installed app yet, redirecting to OAuth", {
138
+ shop,
139
+ });
140
+ if (isEmbedded) {
141
+ redirectWithExitIframe({api, config, logger}, request, shop!);
142
+ } else {
143
+ throw await beginAuth({api, config, logger}, request, false, shop!);
144
+ }
145
+ }
146
+
147
+ shop = shop || offlineSession.shop;
148
+
149
+ if (config.isEmbeddedApp && !isEmbedded) {
150
+ try {
151
+ logger.debug('Ensuring offline session is valid before embedding', {
152
+ shop,
153
+ });
154
+ await this.testSession(offlineSession);
155
+
156
+ logger.debug('Offline session is still valid, embedding app', {shop});
157
+ } catch (error) {
158
+ await this.handleInvalidOfflineSession(error, request, shop);
159
+ }
160
+ }
161
+ }
162
+
163
+ private async handleAuthBeginRequest(
164
+ request: Request,
165
+ shop: string,
166
+ ): Promise<never> {
167
+ const {api, config, logger} = this;
168
+
169
+ logger.info('Handling OAuth begin request', {shop});
170
+
171
+ // If we're loading from an iframe, we need to break out of it
172
+ if (
173
+ config.isEmbeddedApp &&
174
+ request.headers.get('Sec-Fetch-Dest') === 'iframe'
175
+ ) {
176
+ logger.debug('Auth request in iframe detected, exiting iframe', {shop});
177
+ throw redirectWithExitIframe({api, config, logger}, request, shop);
178
+ } else {
179
+ throw await beginAuth({api, config, logger}, request, false, shop);
180
+ }
181
+ }
182
+
183
+ private async handleAuthCallbackRequest(
184
+ request: Request,
185
+ shop: string,
186
+ ): Promise<never> {
187
+ const {api, config, logger} = this;
188
+
189
+ logger.info('Handling OAuth callback request', {shop});
190
+
191
+ try {
192
+ const {session, headers: responseHeaders} = await api.auth.callback({
193
+ rawRequest: request,
194
+ expiring: config.future.expiringOfflineAccessTokens,
195
+ });
196
+
197
+ await config.sessionStorage!.storeSession(session);
198
+
199
+ if (config.useOnlineTokens && !session.isOnline) {
200
+ logger.info('Requesting online access token for offline session', {
201
+ shop,
202
+ });
203
+ await beginAuth({api, config, logger}, request, true, shop);
204
+ }
205
+
206
+ logger.debug('Request is valid, loaded session from OAuth callback', {
207
+ shop: session.shop,
208
+ isOnline: session.isOnline,
209
+ });
210
+
211
+ await triggerAfterAuthHook({api, config, logger}, session, request, this);
212
+
213
+ throw await redirectToShopifyOrAppRoot(
214
+ request,
215
+ {api, config, logger},
216
+ responseHeaders,
217
+ );
218
+ } catch (error) {
219
+ if (error instanceof Response) throw error;
220
+
221
+ throw await this.oauthCallbackError(error, request, shop);
222
+ }
223
+ }
224
+
225
+ private async getOfflineSession(
226
+ request: Request,
227
+ ): Promise<Session | undefined> {
228
+ const offlineId = await this.getOfflineSessionId(request);
229
+ return this.config.sessionStorage!.loadSession(offlineId!);
230
+ }
231
+
232
+ private async hasValidOfflineId(request: Request) {
233
+ return Boolean(await this.getOfflineSessionId(request));
234
+ }
235
+
236
+ private async getOfflineSessionId(
237
+ request: Request,
238
+ ): Promise<string | undefined> {
239
+ const {api} = this;
240
+ const url = new URL(request.url);
241
+ const shop = url.searchParams.get('shop');
242
+
243
+ return shop
244
+ ? api.session.getOfflineId(shop)
245
+ : api.session.getCurrentId({isOnline: false, rawRequest: request});
246
+ }
247
+
248
+ private async testSession(session: Session): Promise<void> {
249
+ const {api} = this;
250
+
251
+ const client = new api.clients.Graphql({
252
+ session,
253
+ });
254
+
255
+ await client.request(`#graphql
256
+ query shopifyAppShopName {
257
+ shop {
258
+ name
259
+ }
260
+ }
261
+ `);
262
+ }
263
+
264
+ private async oauthCallbackError(
265
+ error: Error,
266
+ request: Request,
267
+ shop: string,
268
+ ) {
269
+ const {logger} = this;
270
+ logger.error('Error during OAuth callback', {shop, error: error.message});
271
+
272
+ if (error instanceof CookieNotFound) {
273
+ return this.handleAuthBeginRequest(request, shop);
274
+ }
275
+
276
+ if (
277
+ error instanceof InvalidHmacError ||
278
+ error instanceof InvalidOAuthError
279
+ ) {
280
+ return new Response(undefined, {
281
+ status: 400,
282
+ statusText: 'Invalid OAuth Request',
283
+ });
284
+ }
285
+
286
+ return new Response(undefined, {
287
+ status: 500,
288
+ statusText: 'Internal Server Error',
289
+ });
290
+ }
291
+
292
+ private async handleInvalidOfflineSession(
293
+ error: Error,
294
+ request: Request,
295
+ shop: string,
296
+ ) {
297
+ const {api, logger, config} = this;
298
+ if (error instanceof HttpResponseError) {
299
+ if (error.response.code === 401) {
300
+ logger.info('Shop session is no longer valid, redirecting to OAuth', {
301
+ shop,
302
+ });
303
+ throw await beginAuth({api, config, logger}, request, false, shop);
304
+ } else {
305
+ const message = JSON.stringify(error.response.body, null, 2);
306
+ logger.error(`Unexpected error during session validation: ${message}`, {
307
+ shop,
308
+ });
309
+
310
+ throw new Response(undefined, {
311
+ status: error.response.code,
312
+ statusText: error.response.statusText,
313
+ });
314
+ }
315
+ } else if (error instanceof GraphqlQueryError) {
316
+ const context: Record<string, string> = {shop};
317
+ if (error.response) {
318
+ context.response = JSON.stringify(error.body);
319
+ }
320
+
321
+ logger.error(
322
+ `Unexpected error during session validation: ${error.message}`,
323
+ context,
324
+ );
325
+
326
+ throw new Response(undefined, {
327
+ status: 500,
328
+ statusText: 'Internal Server Error',
329
+ });
330
+ }
331
+ }
332
+ }