@shopify/shopify-app-remix 4.1.1 → 4.2.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 (206) hide show
  1. package/dist/cjs/react/components/AppProvider/AppProvider.js.map +1 -1
  2. package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js.map +1 -1
  3. package/dist/cjs/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
  4. package/dist/cjs/server/authenticate/admin/strategies/merchant-custom-app.js.map +1 -1
  5. package/dist/cjs/server/authenticate/admin/strategies/token-exchange.js.map +1 -1
  6. package/dist/cjs/server/authenticate/webhooks/authenticate.js +34 -10
  7. package/dist/cjs/server/authenticate/webhooks/authenticate.js.map +1 -1
  8. package/dist/cjs/server/version.js +1 -1
  9. package/dist/cjs/server/version.js.map +1 -1
  10. package/dist/esm/react/components/AppProvider/AppProvider.mjs.map +1 -1
  11. package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs.map +1 -1
  12. package/dist/esm/server/authenticate/admin/strategies/auth-code-flow.mjs.map +1 -1
  13. package/dist/esm/server/authenticate/admin/strategies/merchant-custom-app.mjs.map +1 -1
  14. package/dist/esm/server/authenticate/admin/strategies/token-exchange.mjs.map +1 -1
  15. package/dist/esm/server/authenticate/webhooks/authenticate.mjs +35 -11
  16. package/dist/esm/server/authenticate/webhooks/authenticate.mjs.map +1 -1
  17. package/dist/esm/server/version.mjs +1 -1
  18. package/dist/esm/server/version.mjs.map +1 -1
  19. package/dist/ts/react/components/AppProvider/AppProvider.d.ts.map +1 -1
  20. package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts.map +1 -1
  21. package/dist/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
  22. package/dist/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
  23. package/dist/ts/server/authenticate/admin/strategies/merchant-custom-app.d.ts.map +1 -1
  24. package/dist/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -1
  25. package/dist/ts/server/authenticate/admin/types.d.ts.map +1 -1
  26. package/dist/ts/server/authenticate/public/extension/types.d.ts.map +1 -1
  27. package/dist/ts/server/authenticate/webhooks/authenticate.d.ts.map +1 -1
  28. package/dist/ts/server/authenticate/webhooks/types.d.ts +34 -2
  29. package/dist/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
  30. package/dist/ts/server/config-types.d.ts.map +1 -1
  31. package/dist/ts/server/version.d.ts +1 -1
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +18 -14
  34. package/src/react/components/AppProvider/AppProvider.doc.ts +35 -0
  35. package/src/react/components/AppProvider/AppProvider.tsx +147 -0
  36. package/src/react/components/AppProvider/index.ts +1 -0
  37. package/src/react/components/AppProxyForm/AppProxyForm.doc.ts +28 -0
  38. package/src/react/components/AppProxyForm/AppProxyForm.tsx +82 -0
  39. package/src/react/components/AppProxyForm/index.ts +1 -0
  40. package/src/react/components/AppProxyLink/AppProxyLink.doc.ts +28 -0
  41. package/src/react/components/AppProxyLink/AppProxyLink.tsx +58 -0
  42. package/src/react/components/AppProxyLink/index.ts +1 -0
  43. package/src/react/components/AppProxyProvider/AppProxyProvider.doc.ts +40 -0
  44. package/src/react/components/AppProxyProvider/AppProxyProvider.tsx +100 -0
  45. package/src/react/components/AppProxyProvider/index.ts +5 -0
  46. package/src/react/components/RemixPolarisLink.tsx +14 -0
  47. package/src/react/components/index.ts +4 -0
  48. package/src/react/const.ts +2 -0
  49. package/src/react/index.ts +1 -0
  50. package/src/server/__test-helpers/const.ts +14 -0
  51. package/src/server/__test-helpers/expect-admin-api-client.ts +51 -0
  52. package/src/server/__test-helpers/expect-begin-auth-redirect.ts +22 -0
  53. package/src/server/__test-helpers/expect-document-request-headers.ts +25 -0
  54. package/src/server/__test-helpers/expect-exit-iframe.ts +37 -0
  55. package/src/server/__test-helpers/expect-login-redirect.ts +8 -0
  56. package/src/server/__test-helpers/expect-storefront-api-client.ts +49 -0
  57. package/src/server/__test-helpers/expect-token-refresh.ts +188 -0
  58. package/src/server/__test-helpers/get-hmac.ts +7 -0
  59. package/src/server/__test-helpers/get-jwt.ts +10 -0
  60. package/src/server/__test-helpers/get-thrown-error.ts +19 -0
  61. package/src/server/__test-helpers/get-thrown-response.ts +17 -0
  62. package/src/server/__test-helpers/index.ts +21 -0
  63. package/src/server/__test-helpers/mock-graphql-request.ts +60 -0
  64. package/src/server/__test-helpers/request-mock.ts +169 -0
  65. package/src/server/__test-helpers/setup-embedded-flow.ts +30 -0
  66. package/src/server/__test-helpers/setup-fetch-flow.ts +29 -0
  67. package/src/server/__test-helpers/setup-non-embedded-flow.ts +31 -0
  68. package/src/server/__test-helpers/setup-valid-request.ts +78 -0
  69. package/src/server/__test-helpers/setup-valid-session.ts +28 -0
  70. package/src/server/__test-helpers/sign-request-cookie.ts +21 -0
  71. package/src/server/__test-helpers/test-config.ts +56 -0
  72. package/src/server/adapters/node/index.ts +13 -0
  73. package/src/server/adapters/vercel/index.ts +13 -0
  74. package/src/server/authenticate/admin/authenticate.admin.doc.example.ts +34 -0
  75. package/src/server/authenticate/admin/authenticate.admin.doc.ts +51 -0
  76. package/src/server/authenticate/admin/authenticate.ts +232 -0
  77. package/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts +29 -0
  78. package/src/server/authenticate/admin/billing/cancel.ts +38 -0
  79. package/src/server/authenticate/admin/billing/check.ts +41 -0
  80. package/src/server/authenticate/admin/billing/create-usage-record.ts +36 -0
  81. package/src/server/authenticate/admin/billing/helpers.ts +42 -0
  82. package/src/server/authenticate/admin/billing/index.ts +6 -0
  83. package/src/server/authenticate/admin/billing/request.ts +64 -0
  84. package/src/server/authenticate/admin/billing/require.ts +58 -0
  85. package/src/server/authenticate/admin/billing/types.ts +613 -0
  86. package/src/server/authenticate/admin/billing/update-usage-subscription-capped-amount.ts +55 -0
  87. package/src/server/authenticate/admin/helpers/begin-auth.ts +17 -0
  88. package/src/server/authenticate/admin/helpers/create-admin-api-context.ts +20 -0
  89. package/src/server/authenticate/admin/helpers/ensure-app-is-embedded-if-required.ts +18 -0
  90. package/src/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.ts +25 -0
  91. package/src/server/authenticate/admin/helpers/handle-client-error.ts +45 -0
  92. package/src/server/authenticate/admin/helpers/index.ts +14 -0
  93. package/src/server/authenticate/admin/helpers/redirect-to-auth-page.ts +28 -0
  94. package/src/server/authenticate/admin/helpers/redirect-to-bounce-page.ts +23 -0
  95. package/src/server/authenticate/admin/helpers/redirect-to-install-page.ts +53 -0
  96. package/src/server/authenticate/admin/helpers/redirect-to-shopify-or-app-root.ts +21 -0
  97. package/src/server/authenticate/admin/helpers/redirect-with-app-bridge-headers.ts +13 -0
  98. package/src/server/authenticate/admin/helpers/redirect-with-exitiframe.ts +28 -0
  99. package/src/server/authenticate/admin/helpers/redirect.ts +164 -0
  100. package/src/server/authenticate/admin/helpers/render-app-bridge.ts +46 -0
  101. package/src/server/authenticate/admin/helpers/trigger-after-auth-hook.ts +29 -0
  102. package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +67 -0
  103. package/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +46 -0
  104. package/src/server/authenticate/admin/scope/authenticate.admin.scopes.doc.ts +28 -0
  105. package/src/server/authenticate/admin/scope/client/fetch-scopes-details.ts +43 -0
  106. package/src/server/authenticate/admin/scope/client/revoke-scopes.ts +38 -0
  107. package/src/server/authenticate/admin/scope/factory.ts +21 -0
  108. package/src/server/authenticate/admin/scope/query.ts +51 -0
  109. package/src/server/authenticate/admin/scope/request.ts +32 -0
  110. package/src/server/authenticate/admin/scope/revoke.ts +48 -0
  111. package/src/server/authenticate/admin/scope/types.ts +126 -0
  112. package/src/server/authenticate/admin/strategies/auth-code-flow.ts +332 -0
  113. package/src/server/authenticate/admin/strategies/merchant-custom-app.ts +65 -0
  114. package/src/server/authenticate/admin/strategies/token-exchange.ts +188 -0
  115. package/src/server/authenticate/admin/strategies/types.ts +29 -0
  116. package/src/server/authenticate/admin/types.ts +216 -0
  117. package/src/server/authenticate/const.ts +15 -0
  118. package/src/server/authenticate/flow/authenticate.flow.doc.example.ts +39 -0
  119. package/src/server/authenticate/flow/authenticate.flow.doc.ts +47 -0
  120. package/src/server/authenticate/flow/authenticate.ts +68 -0
  121. package/src/server/authenticate/flow/types.ts +85 -0
  122. package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts +52 -0
  123. package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.ts +48 -0
  124. package/src/server/authenticate/fulfillment-service/authenticate.ts +82 -0
  125. package/src/server/authenticate/fulfillment-service/types.ts +82 -0
  126. package/src/server/authenticate/helpers/add-response-headers.ts +43 -0
  127. package/src/server/authenticate/helpers/app-bridge-url.ts +10 -0
  128. package/src/server/authenticate/helpers/ensure-cors-headers.ts +38 -0
  129. package/src/server/authenticate/helpers/get-session-token-header.ts +11 -0
  130. package/src/server/authenticate/helpers/get-shop-from-request.ts +4 -0
  131. package/src/server/authenticate/helpers/idempotent-promise-handler.ts +45 -0
  132. package/src/server/authenticate/helpers/index.ts +10 -0
  133. package/src/server/authenticate/helpers/invalidate-access-token.ts +17 -0
  134. package/src/server/authenticate/helpers/reject-bot-request.ts +27 -0
  135. package/src/server/authenticate/helpers/respond-to-invalid-session-token.ts +28 -0
  136. package/src/server/authenticate/helpers/respond-to-options-request.ts +26 -0
  137. package/src/server/authenticate/helpers/validate-session-token.ts +40 -0
  138. package/src/server/authenticate/login/login.ts +53 -0
  139. package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.example.ts +27 -0
  140. package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts +55 -0
  141. package/src/server/authenticate/public/appProxy/authenticate.ts +150 -0
  142. package/src/server/authenticate/public/appProxy/types.ts +215 -0
  143. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.offers.ts +21 -0
  144. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.ts +17 -0
  145. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.ts +49 -0
  146. package/src/server/authenticate/public/checkout/authenticate.ts +13 -0
  147. package/src/server/authenticate/public/checkout/types.ts +65 -0
  148. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.offers.ts +36 -0
  149. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.ts +18 -0
  150. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.ts +49 -0
  151. package/src/server/authenticate/public/customer-account/authenticate.ts +13 -0
  152. package/src/server/authenticate/public/customer-account/types.ts +65 -0
  153. package/src/server/authenticate/public/extension/authenticate.ts +54 -0
  154. package/src/server/authenticate/public/extension/types.ts +11 -0
  155. package/src/server/authenticate/public/factory.ts +24 -0
  156. package/src/server/authenticate/public/index.ts +1 -0
  157. package/src/server/authenticate/public/pos/authenticate.ts +8 -0
  158. package/src/server/authenticate/public/pos/types.ts +65 -0
  159. package/src/server/authenticate/public/types.ts +96 -0
  160. package/src/server/authenticate/webhooks/authenticate.ts +105 -0
  161. package/src/server/authenticate/webhooks/authenticate.webhooks.doc.example.ts +43 -0
  162. package/src/server/authenticate/webhooks/authenticate.webhooks.doc.ts +39 -0
  163. package/src/server/authenticate/webhooks/index.ts +1 -0
  164. package/src/server/authenticate/webhooks/register.ts +48 -0
  165. package/src/server/authenticate/webhooks/types.ts +266 -0
  166. package/src/server/boundary/error.tsx +14 -0
  167. package/src/server/boundary/headers.ts +15 -0
  168. package/src/server/boundary/index.ts +39 -0
  169. package/src/server/boundary/types.ts +4 -0
  170. package/src/server/clients/admin/authenticate.admin.api.doc.ts +34 -0
  171. package/src/server/clients/admin/factory.ts +22 -0
  172. package/src/server/clients/admin/graphql.ts +39 -0
  173. package/src/server/clients/admin/index.ts +2 -0
  174. package/src/server/clients/admin/types.ts +120 -0
  175. package/src/server/clients/index.ts +2 -0
  176. package/src/server/clients/storefront/authenticate.storefront.api.doc.ts +34 -0
  177. package/src/server/clients/storefront/factory.ts +32 -0
  178. package/src/server/clients/storefront/index.ts +2 -0
  179. package/src/server/clients/storefront/types.ts +78 -0
  180. package/src/server/clients/types.ts +47 -0
  181. package/src/server/config-types.ts +309 -0
  182. package/src/server/errors.ts +3 -0
  183. package/src/server/future/flags.ts +62 -0
  184. package/src/server/helpers/create-or-load-offline-session.ts +19 -0
  185. package/src/server/helpers/ensure-offline-token-is-not-expired.ts +32 -0
  186. package/src/server/helpers/ensure-valid-offline-session.ts +15 -0
  187. package/src/server/helpers/index.ts +4 -0
  188. package/src/server/helpers/refresh-token.ts +35 -0
  189. package/src/server/index.ts +24 -0
  190. package/src/server/override-logger.ts +42 -0
  191. package/src/server/shopify-app.doc.ts +45 -0
  192. package/src/server/shopify-app.ts +215 -0
  193. package/src/server/test-helpers/index.ts +1 -0
  194. package/src/server/test-helpers/test-config.ts +79 -0
  195. package/src/server/types-contexts.ts +44 -0
  196. package/src/server/types.ts +507 -0
  197. package/src/server/unauthenticated/admin/factory.ts +23 -0
  198. package/src/server/unauthenticated/admin/index.ts +1 -0
  199. package/src/server/unauthenticated/admin/types.ts +79 -0
  200. package/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts +40 -0
  201. package/src/server/unauthenticated/storefront/factory.ts +28 -0
  202. package/src/server/unauthenticated/storefront/index.ts +1 -0
  203. package/src/server/unauthenticated/storefront/types.ts +105 -0
  204. package/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts +40 -0
  205. package/src/server/unauthenticated/types.ts +65 -0
  206. package/src/server/version.ts +1 -0
@@ -0,0 +1,36 @@
1
+ // Most apps would load this from their database
2
+ export function getOffers(shop: string, customerID: string) {
3
+ const offers: Record<string, any[]> = {
4
+ 'shop.com': [
5
+ {
6
+ id: '1',
7
+ title: '10% off',
8
+ price: 10,
9
+ type: 'percentage',
10
+ customerId: 'gid://shopify/Customer/1001',
11
+ },
12
+ {
13
+ id: '2',
14
+ title: 'Free shipping',
15
+ price: 0,
16
+ type: 'shipping',
17
+ customerId: 'gid://shopify/Customer/1001',
18
+ },
19
+ {
20
+ id: '3',
21
+ title: '5% off',
22
+ price: 5,
23
+ type: 'percentage',
24
+ customerId: 'gid://shopify/Customer/1001',
25
+ },
26
+ ],
27
+ };
28
+
29
+ const allOffers = offers[shop] || [];
30
+ // Filter offers to include only those that match the customerId
31
+ const filteredOffers = allOffers.filter(
32
+ (offer) => offer.customerId === customerID,
33
+ );
34
+
35
+ return filteredOffers;
36
+ }
@@ -0,0 +1,18 @@
1
+ import type {ActionFunctionArgs, LoaderFunctionArgs} from '@remix-run/node';
2
+ import {json} from '@remix-run/node';
3
+
4
+ import {authenticate} from '../shopify.server';
5
+ import {getOffers} from '../offers.server';
6
+
7
+ // The loader responds to preflight requests from Shopify
8
+ export const loader = async ({request}: LoaderFunctionArgs) => {
9
+ await authenticate.public.customerAccount(request);
10
+ };
11
+
12
+ export const action = async ({request}: ActionFunctionArgs) => {
13
+ const {cors, sessionToken} = await authenticate.public.customerAccount(request);
14
+
15
+ // Get offers for the customer
16
+ const offers = getOffers(sessionToken.des, sessionToken.sub);
17
+ return cors(json({offers}));
18
+ };
@@ -0,0 +1,49 @@
1
+ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
2
+
3
+ const data: ReferenceEntityTemplateSchema = {
4
+ name: 'Customer account',
5
+ description:
6
+ 'The `authenticate.public.customerAccount` function ensures that customer account extension requests are coming from Shopify, and returns helpers to respond with the correct headers.',
7
+ category: 'Authenticate',
8
+ subCategory: 'Public',
9
+ type: 'object',
10
+ isVisualComponent: false,
11
+ definitions: [
12
+ {
13
+ title: 'authenticate.public.customerAccount',
14
+ description:
15
+ 'Authenticates requests coming from Shopify customer account extensions.',
16
+ type: 'AuthenticateCustomerAccount',
17
+ },
18
+ ],
19
+ defaultExample: {
20
+ description: 'Authenticate and return offers for the customer',
21
+ codeblock: {
22
+ title: 'Authenticate and return offers for the customer',
23
+ tabs: [
24
+ {
25
+ title: '/app/routes/**.ts',
26
+ language: 'typescript',
27
+ code: './authenticate.public.customerAccount.doc.example.ts',
28
+ },
29
+ {
30
+ title: '/app/offers.server.ts',
31
+ language: 'typescript',
32
+ code: './authenticate.public.customerAccount.doc.example.offers.ts',
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ jsDocTypeExamples: ['CustomerAccountContext'],
38
+ related: [
39
+ {
40
+ name: 'Session token API',
41
+ subtitle:
42
+ 'Customer account UI extensions API for interacting with session tokens.',
43
+ url: '/docs/api/customer-account-ui-extensions/latest/apis/session-token',
44
+ type: 'shopify',
45
+ },
46
+ ],
47
+ };
48
+
49
+ export default data;
@@ -0,0 +1,13 @@
1
+ import {BasicParams} from '../../../types';
2
+ import {authenticateExtensionFactory} from '../extension/authenticate';
3
+
4
+ import {AuthenticateCustomerAccount} from './types';
5
+
6
+ export function authenticateCustomerAccountFactory(
7
+ params: BasicParams,
8
+ ): AuthenticateCustomerAccount {
9
+ return authenticateExtensionFactory(
10
+ params,
11
+ 'customer account',
12
+ ) as AuthenticateCustomerAccount;
13
+ }
@@ -0,0 +1,65 @@
1
+ import {JwtPayload} from '@shopify/shopify-api';
2
+
3
+ import {EnsureCORSFunction} from '../../helpers/ensure-cors-headers';
4
+
5
+ export type AuthenticateCustomerAccount = (
6
+ request: Request,
7
+ options?: AuthenticateCustomerAccountOptions,
8
+ ) => Promise<CustomerAccountContext>;
9
+
10
+ export interface AuthenticateCustomerAccountOptions {
11
+ corsHeaders?: string[];
12
+ }
13
+
14
+ /**
15
+ * Authenticated Context for a customer account extension request
16
+ */
17
+ export interface CustomerAccountContext {
18
+ /**
19
+ * The decoded and validated session token for the request
20
+ *
21
+ * Refer to the OAuth docs for the [session token payload](https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload).
22
+ *
23
+ * @example
24
+ * <caption>Using the decoded session token.</caption>
25
+ * <description>Get store-specific data using the `sessionToken` object.</description>
26
+ * ```ts
27
+ * // app/routes/public/my-route.ts
28
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
29
+ * import { authenticate } from "../shopify.server";
30
+ * import { getMyAppData } from "~/db/model.server";
31
+ *
32
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
33
+ * const { sessionToken } = await authenticate.public.customerAccount(
34
+ * request
35
+ * );
36
+ * return json(await getMyAppData({shop: sessionToken.dest}));
37
+ * };
38
+ * ```
39
+ */
40
+ sessionToken: JwtPayload;
41
+
42
+ /**
43
+ * A function that ensures the CORS headers are set correctly for the response.
44
+ *
45
+ * @example
46
+ * <caption>Setting CORS headers for a public request.</caption>
47
+ * <description>Use the `cors` helper to ensure your app can respond to customer account extension requests.</description>
48
+ * ```ts
49
+ * // app/routes/public/my-route.ts
50
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
51
+ * import { authenticate } from "../shopify.server";
52
+ * import { getMyAppData } from "~/db/model.server";
53
+ *
54
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
55
+ * const { sessionToken, cors } = await authenticate.public.customerAccount(
56
+ * request,
57
+ * { corsHeaders: ["X-My-Custom-Header"] }
58
+ * );
59
+ * const data = await getMyAppData({shop: sessionToken.dest});
60
+ * return cors(json(data));
61
+ * };
62
+ * ```
63
+ */
64
+ cors: EnsureCORSFunction;
65
+ }
@@ -0,0 +1,54 @@
1
+ import {BasicParams} from '../../../types';
2
+ import {
3
+ respondToBotRequest,
4
+ respondToOptionsRequest,
5
+ getSessionTokenHeader,
6
+ validateSessionToken,
7
+ ensureCORSHeadersFactory,
8
+ getShopFromRequest,
9
+ } from '../../helpers';
10
+
11
+ import {AuthenticateExtension, ExtensionContext} from './types';
12
+
13
+ export function authenticateExtensionFactory(
14
+ params: BasicParams,
15
+ requestType: string,
16
+ ): AuthenticateExtension {
17
+ return async function authenticateExtension(
18
+ request,
19
+ options = {},
20
+ ): Promise<ExtensionContext> {
21
+ const {logger} = params;
22
+
23
+ const corsHeaders = options.corsHeaders ?? [];
24
+
25
+ respondToBotRequest(params, request);
26
+ respondToOptionsRequest(params, request, corsHeaders);
27
+
28
+ const sessionTokenHeader = getSessionTokenHeader(request);
29
+
30
+ logger.info(`Authenticating ${requestType} request`, {
31
+ shop: getShopFromRequest(request),
32
+ });
33
+
34
+ if (!sessionTokenHeader) {
35
+ logger.debug('Request did not contain a session token', {
36
+ shop: getShopFromRequest(request),
37
+ });
38
+ throw new Response(undefined, {
39
+ status: 401,
40
+ statusText: 'Unauthorized',
41
+ });
42
+ }
43
+
44
+ return {
45
+ sessionToken: await validateSessionToken(
46
+ params,
47
+ request,
48
+ sessionTokenHeader,
49
+ {checkAudience: false, retryRequest: false},
50
+ ),
51
+ cors: ensureCORSHeadersFactory(params, request, corsHeaders),
52
+ };
53
+ };
54
+ }
@@ -0,0 +1,11 @@
1
+ export type AuthenticateExtension = (
2
+ request: Request,
3
+ options?: AuthenticateExtensionOptions,
4
+ ) => Promise<ExtensionContext>;
5
+
6
+ export interface AuthenticateExtensionOptions {
7
+ corsHeaders?: string[];
8
+ }
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
11
+ export interface ExtensionContext {}
@@ -0,0 +1,24 @@
1
+ import {BasicParams} from '../../types';
2
+
3
+ import {authenticateCheckoutFactory} from './checkout/authenticate';
4
+ import {authenticateAppProxyFactory} from './appProxy/authenticate';
5
+ import {authenticateCustomerAccountFactory} from './customer-account/authenticate';
6
+ import {AuthenticatePublic} from './types';
7
+ import {authenticatePOSFactory} from './pos/authenticate';
8
+
9
+ export function authenticatePublicFactory(params: BasicParams) {
10
+ const authenticateCheckout = authenticateCheckoutFactory(params);
11
+ const authenticateAppProxy = authenticateAppProxyFactory(params);
12
+ const authenticateCustomerAccount =
13
+ authenticateCustomerAccountFactory(params);
14
+ const authenticatePOS = authenticatePOSFactory(params);
15
+
16
+ const context: AuthenticatePublic = {
17
+ checkout: authenticateCheckout,
18
+ appProxy: authenticateAppProxy,
19
+ customerAccount: authenticateCustomerAccount,
20
+ pos: authenticatePOS,
21
+ };
22
+
23
+ return context;
24
+ }
@@ -0,0 +1 @@
1
+ export * from './factory';
@@ -0,0 +1,8 @@
1
+ import {BasicParams} from '../../../types';
2
+ import {authenticateExtensionFactory} from '../extension/authenticate';
3
+
4
+ import type {AuthenticatePOS} from './types';
5
+
6
+ export function authenticatePOSFactory(params: BasicParams): AuthenticatePOS {
7
+ return authenticateExtensionFactory(params, 'pos') as AuthenticatePOS;
8
+ }
@@ -0,0 +1,65 @@
1
+ import {JwtPayload} from '@shopify/shopify-api';
2
+
3
+ import {EnsureCORSFunction} from '../../helpers/ensure-cors-headers';
4
+
5
+ export type AuthenticatePOS = (
6
+ request: Request,
7
+ options?: AuthenticatePOSOptions,
8
+ ) => Promise<POSContext>;
9
+
10
+ export interface AuthenticatePOSOptions {
11
+ corsHeaders?: string[];
12
+ }
13
+
14
+ /**
15
+ * Authenticated Context for a POS UI extension request
16
+ */
17
+ export interface POSContext {
18
+ /**
19
+ * The decoded and validated session token for the request
20
+ *
21
+ * Refer to the OAuth docs for the [session token payload](https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload).
22
+ *
23
+ * @example
24
+ * <caption>Using the decoded session token.</caption>
25
+ * <description>Get store-specific data using the `sessionToken` object.</description>
26
+ * ```ts
27
+ * // app/routes/public/my-route.ts
28
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
29
+ * import { authenticate } from "../shopify.server";
30
+ * import { getMyAppData } from "~/db/model.server";
31
+ *
32
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
33
+ * const { sessionToken } = await authenticate.public.pos(
34
+ * request
35
+ * );
36
+ * return json(await getMyAppData({shop: sessionToken.dest}));
37
+ * };
38
+ * ```
39
+ */
40
+ sessionToken: JwtPayload;
41
+
42
+ /**
43
+ * A function that ensures the CORS headers are set correctly for the response.
44
+ *
45
+ * @example
46
+ * <caption>Setting CORS headers for a public request.</caption>
47
+ * <description>Use the `cors` helper to ensure your app can respond to POS UI extension requests.</description>
48
+ * ```ts
49
+ * // app/routes/public/my-route.ts
50
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
51
+ * import { authenticate } from "../shopify.server";
52
+ * import { getMyAppData } from "~/db/model.server";
53
+ *
54
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
55
+ * const { sessionToken, cors } = await authenticate.public.pos(
56
+ * request,
57
+ * { corsHeaders: ["X-My-Custom-Header"] }
58
+ * );
59
+ * const data = await getMyAppData({shop: sessionToken.dest});
60
+ * return cors(json(data));
61
+ * };
62
+ * ```
63
+ */
64
+ cors: EnsureCORSFunction;
65
+ }
@@ -0,0 +1,96 @@
1
+ import type {AuthenticateCheckout} from './checkout/types';
2
+ import type {AuthenticateAppProxy} from './appProxy/types';
3
+ import type {AuthenticateCustomerAccount} from './customer-account/types';
4
+ import type {AuthenticatePOS} from './pos/types';
5
+
6
+ // Eventually this will be just the `{}` bit without `AuthenticateCheckout &`
7
+ // We have this is because in v1 public WAS the only public authenticate method
8
+ // But it became tightly coupled to authenticating Checkout requests.
9
+ // In V2 you will have only public.checkout() and public.appProxy(), no public()
10
+
11
+ export interface AuthenticatePublic {
12
+ /**
13
+ * Authenticate a request from a checkout extension
14
+ *
15
+ * @example
16
+ * <caption>Authenticating a checkout extension request</caption>
17
+ * ```ts
18
+ * // /app/routes/public/widgets.ts
19
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
20
+ * import { authenticate } from "../shopify.server";
21
+ *
22
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
23
+ * const { sessionToken, cors } = await authenticate.public.checkout(
24
+ * request,
25
+ * );
26
+ * return cors(json({my: "data", shop: sessionToken.dest}));
27
+ * };
28
+ * ```
29
+ */
30
+ checkout: AuthenticateCheckout;
31
+
32
+ /**
33
+ * Authenticate a request from an app proxy
34
+ *
35
+ * @example
36
+ * <caption>Authenticating an app proxy request</caption>
37
+ * ```ts
38
+ * // /app/routes/public/widgets.ts
39
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
40
+ * import { authenticate } from "../shopify.server";
41
+ *
42
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
43
+ * await authenticate.public.appProxy(
44
+ * request,
45
+ * );
46
+ *
47
+ * const {searchParams} = new URL(request.url);
48
+ * const shop = searchParams.get("shop");
49
+ * const customerId = searchParams.get("logged_in_customer_id")
50
+ *
51
+ * return json({my: "data", shop, customerId});
52
+ * };
53
+ * ```
54
+ */
55
+ appProxy: AuthenticateAppProxy;
56
+
57
+ /**
58
+ * Authenticate a request from a customer account extension
59
+ *
60
+ * @example
61
+ * <caption>Authenticating a customer account extension request</caption>
62
+ * ```ts
63
+ * // /app/routes/public/widgets.ts
64
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
65
+ * import { authenticate } from "../shopify.server";
66
+ *
67
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
68
+ * const { sessionToken, cors } = await authenticate.public.customerAccount(
69
+ * request,
70
+ * );
71
+ * return cors(json({my: "data", shop: sessionToken.dest}));
72
+ * };
73
+ * ```
74
+ */
75
+ customerAccount: AuthenticateCustomerAccount;
76
+
77
+ /**
78
+ * Authenticate a request from a POS UI extension
79
+ *
80
+ * @example
81
+ * <caption>Authenticating a POS UI extension request</caption>
82
+ * ```ts
83
+ * // /app/routes/public/widgets.ts
84
+ * import { LoaderFunctionArgs, json } from "@remix-run/node";
85
+ * import { authenticate } from "../shopify.server";
86
+ *
87
+ * export const loader = async ({ request }: LoaderFunctionArgs) => {
88
+ * const { sessionToken, cors } = await authenticate.public.pos(
89
+ * request,
90
+ * );
91
+ * return cors(json({my: "data", shop: sessionToken.dest}));
92
+ * };
93
+ * ```
94
+ */
95
+ pos: AuthenticatePOS;
96
+ }
@@ -0,0 +1,105 @@
1
+ import {WebhookValidationErrorReason, WebhookType} from '@shopify/shopify-api';
2
+
3
+ import type {BasicParams} from '../../types';
4
+ import {adminClientFactory} from '../../clients';
5
+ import {handleClientErrorFactory} from '../admin/helpers';
6
+ import {ensureValidOfflineSession} from '../../helpers';
7
+
8
+ import type {
9
+ AuthenticateWebhook,
10
+ WebhookContext,
11
+ WebhookContextWithoutSession,
12
+ } from './types';
13
+
14
+ export function authenticateWebhookFactory<Topics extends string>(
15
+ params: BasicParams,
16
+ ): AuthenticateWebhook<Topics> {
17
+ const {api, logger} = params;
18
+
19
+ return async function authenticate(
20
+ request: Request,
21
+ ): Promise<WebhookContext<Topics>> {
22
+ if (request.method !== 'POST') {
23
+ logger.debug(
24
+ 'Received a non-POST request for a webhook. Only POST requests are allowed.',
25
+ {url: request.url, method: request.method},
26
+ );
27
+ throw new Response(undefined, {
28
+ status: 405,
29
+ statusText: 'Method not allowed',
30
+ });
31
+ }
32
+
33
+ const rawBody = await request.text();
34
+
35
+ const check = await api.webhooks.validate({
36
+ rawBody,
37
+ rawRequest: request,
38
+ });
39
+
40
+ if (!check.valid) {
41
+ if (check.reason === WebhookValidationErrorReason.InvalidHmac) {
42
+ logger.debug('Webhook HMAC validation failed', check);
43
+ throw new Response(undefined, {
44
+ status: 401,
45
+ statusText: 'Unauthorized',
46
+ });
47
+ } else {
48
+ logger.debug('Webhook validation failed', check);
49
+ throw new Response(undefined, {status: 400, statusText: 'Bad Request'});
50
+ }
51
+ }
52
+ const session = await ensureValidOfflineSession(params, check.domain);
53
+
54
+ let webhookContext: WebhookContextWithoutSession<Topics>;
55
+
56
+ if (check.webhookType === WebhookType.Webhooks) {
57
+ webhookContext = {
58
+ apiVersion: check.apiVersion,
59
+ shop: check.domain,
60
+ topic: check.topic as Topics,
61
+ webhookId: check.webhookId,
62
+ payload: JSON.parse(rawBody),
63
+ subTopic: check.subTopic || undefined,
64
+ session: undefined,
65
+ admin: undefined,
66
+ webhookType: check.webhookType,
67
+ name: check.name,
68
+ triggeredAt: check.triggeredAt,
69
+ eventId: check.eventId,
70
+ };
71
+ } else {
72
+ webhookContext = {
73
+ apiVersion: check.apiVersion,
74
+ shop: check.domain,
75
+ topic: check.topic as Topics,
76
+ webhookId: check.eventId,
77
+ payload: JSON.parse(rawBody),
78
+ session: undefined,
79
+ admin: undefined,
80
+ webhookType: check.webhookType,
81
+ handle: check.handle,
82
+ action: check.action,
83
+ resourceId: check.resourceId,
84
+ triggeredAt: check.triggeredAt,
85
+ eventId: check.eventId,
86
+ };
87
+ }
88
+
89
+ if (!session) {
90
+ return webhookContext;
91
+ }
92
+
93
+ const admin = adminClientFactory({
94
+ params,
95
+ session,
96
+ handleClientError: handleClientErrorFactory({request}),
97
+ });
98
+
99
+ return {
100
+ ...webhookContext,
101
+ session,
102
+ admin,
103
+ };
104
+ };
105
+ }
@@ -0,0 +1,43 @@
1
+ import {type ActionFunctionArgs} from '@remix-run/node';
2
+
3
+ import {authenticate} from '../shopify.server';
4
+
5
+ export const action = async ({request}: ActionFunctionArgs) => {
6
+ const {topic, admin, payload, session} = await authenticate.webhook(request);
7
+
8
+ // Webhook requests can trigger after an app is uninstalled
9
+ // If the app is already uninstalled, the session may be undefined.
10
+ if (!session) {
11
+ throw new Response();
12
+ }
13
+
14
+ switch (topic) {
15
+ case 'PRODUCTS_UPDATE':
16
+ await admin.graphql(
17
+ `#graphql
18
+ mutation setMetafield($productId: ID!, $time: String!) {
19
+ metafieldsSet(metafields: {
20
+ ownerId: $productId
21
+ namespace: "my-app",
22
+ key: "webhook_received_at",
23
+ value: $time,
24
+ type: "string",
25
+ }) {
26
+ metafields {
27
+ key
28
+ value
29
+ }
30
+ }
31
+ }
32
+ `,
33
+ {
34
+ variables: {
35
+ productId: payload.admin_graphql_api_id,
36
+ time: new Date().toISOString(),
37
+ },
38
+ },
39
+ );
40
+ }
41
+
42
+ return new Response();
43
+ };
@@ -0,0 +1,39 @@
1
+ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
2
+
3
+ const data: ReferenceEntityTemplateSchema = {
4
+ name: 'Webhook',
5
+ description: 'Contains functions for verifying Shopify webhooks.',
6
+ category: 'Authenticate',
7
+ type: 'object',
8
+ isVisualComponent: false,
9
+ definitions: [
10
+ {
11
+ title: 'authenticate.webhook',
12
+ description: 'Verifies requests coming from Shopify webhooks.',
13
+ type: 'AuthenticateWebhook',
14
+ },
15
+ ],
16
+ defaultExample: {
17
+ description: 'Update a metafield when a product is updated',
18
+ codeblock: {
19
+ title: 'Update a metafield when a product is updated',
20
+ tabs: [
21
+ {
22
+ title: '/app/routes/**.ts',
23
+ language: 'typescript',
24
+ code: './authenticate.webhooks.doc.example.ts',
25
+ },
26
+ ],
27
+ },
28
+ },
29
+ jsDocTypeExamples: ['WebhookContextWithSession'],
30
+ related: [
31
+ {
32
+ name: 'Admin API context',
33
+ subtitle: 'Interact with the Admin API.',
34
+ url: '/docs/api/shopify-app-remix/apis/admin-api',
35
+ },
36
+ ],
37
+ };
38
+
39
+ export default data;
@@ -0,0 +1 @@
1
+ export {registerWebhooksFactory} from './register';