@shopify/shopify-app-remix 4.1.0 → 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,85 @@
1
+ import {Session} from '@shopify/shopify-api';
2
+
3
+ import type {AdminApiContext} from '../../clients';
4
+
5
+ export interface FlowContext {
6
+ /**
7
+ * A session with an offline token for the shop.
8
+ *
9
+ * Returned only if there is a session for the shop.
10
+ *
11
+ * @example
12
+ * <caption>Shopify session for the Flow request.</caption>
13
+ * <description>Use the session associated with this request.</description>
14
+ * ```ts
15
+ * // /app/routes/flow.tsx
16
+ * import { ActionFunctionArgs } from "@remix-run/node";
17
+ * import { authenticate } from "../shopify.server";
18
+ *
19
+ * export const action = async ({ request }: ActionFunctionArgs) => {
20
+ * const { session, admin } = await authenticate.flow(request);
21
+ *
22
+ * console.log(session.id)
23
+ *
24
+ * return new Response();
25
+ * };
26
+ * ```
27
+ */
28
+ session: Session;
29
+
30
+ /**
31
+ * The payload from the Flow request.
32
+ *
33
+ * @example
34
+ * <caption>Flow payload.</caption>
35
+ * <description>Get the request's POST payload.</description>
36
+ * ```ts
37
+ * // /app/routes/flow.tsx
38
+ * import { ActionFunctionArgs } from "@remix-run/node";
39
+ * import { authenticate } from "../shopify.server";
40
+ *
41
+ * export const action = async ({ request }: ActionFunctionArgs) => {
42
+ * const { payload } = await authenticate.flow(request);
43
+ * return new Response();
44
+ * };
45
+ * ```
46
+ */
47
+ payload: any;
48
+
49
+ /**
50
+ * An admin context for the Flow request.
51
+ *
52
+ * Returned only if there is a session for the shop.
53
+ *
54
+ * @example
55
+ * <caption>Flow admin context.</caption>
56
+ * <description>Use the `admin` object in the context to interact with the Admin API.</description>
57
+ * ```ts
58
+ * // /app/routes/flow.tsx
59
+ * import { ActionFunctionArgs } from "@remix-run/node";
60
+ * import { authenticate } from "../shopify.server";
61
+ *
62
+ * export async function action({ request }: ActionFunctionArgs) {
63
+ * const { admin } = await authenticate.flow(request);
64
+ *
65
+ * const response = await admin?.graphql(
66
+ * `#graphql
67
+ * mutation populateProduct($input: ProductInput!) {
68
+ * productCreate(input: $input) {
69
+ * product {
70
+ * id
71
+ * }
72
+ * }
73
+ * }`,
74
+ * { variables: { input: { title: "Product Name" } } }
75
+ * );
76
+ *
77
+ * const productData = await response.json();
78
+ * return json({ data: productData.data });
79
+ * }
80
+ * ```
81
+ */
82
+ admin: AdminApiContext;
83
+ }
84
+
85
+ export type AuthenticateFlow = (request: Request) => Promise<FlowContext>;
@@ -0,0 +1,52 @@
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 {admin, payload} = await authenticate.fulfillmentService(request);
7
+
8
+ const kind = payload.kind;
9
+
10
+ if (kind === 'FULFILLMENT_REQUEST') {
11
+ const response = await admin?.graphql(
12
+ `#graphql
13
+ query {
14
+ shop {
15
+ assignedFulfillmentOrders(first: 10, assignmentStatus: FULFILLMENT_REQUESTED) {
16
+ edges {
17
+ node {
18
+ id
19
+ destination {
20
+ firstName
21
+ lastName
22
+ }
23
+ lineItems(first: 10) {
24
+ edges {
25
+ node {
26
+ id
27
+ productTitle
28
+ sku
29
+ remainingQuantity
30
+ }
31
+ }
32
+ }
33
+ merchantRequests(first: 10, kind: FULFILLMENT_REQUEST) {
34
+ edges {
35
+ node {
36
+ message
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }`,
45
+ );
46
+
47
+ const fulfillments = await response.json();
48
+ console.log(fulfillments);
49
+ }
50
+
51
+ return new Response();
52
+ };
@@ -0,0 +1,48 @@
1
+ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
2
+
3
+ const data: ReferenceEntityTemplateSchema = {
4
+ name: 'Fulfillment Service',
5
+ description:
6
+ 'Contains functions for verifying fulfillment service requests.' +
7
+ '\n\nSee the [fulfillment service documentation](https://shopify.dev/docs/apps/fulfillment/fulfillment-service-apps) for more information.',
8
+ category: 'Authenticate',
9
+ type: 'object',
10
+ isVisualComponent: false,
11
+ definitions: [
12
+ {
13
+ title: 'authenticate.fulfillmentService',
14
+ description:
15
+ 'Verifies requests coming from Shopify to fulfillment service apps',
16
+ type: 'AuthenticateFulfillmentService',
17
+ },
18
+ ],
19
+ defaultExample: {
20
+ description: 'Handle a fulfillment service notification call',
21
+ codeblock: {
22
+ title: 'Consume a fulfillment service notification request',
23
+ tabs: [
24
+ {
25
+ title: '/app/routes/**.ts',
26
+ language: 'typescript',
27
+ code: './authenticate.fulfillment-service.doc.example.ts',
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ jsDocTypeExamples: ['FulfillmentServiceContext'],
33
+ related: [
34
+ {
35
+ name: 'Admin API context',
36
+ subtitle: 'Interact with the Admin API.',
37
+ url: '/docs/api/shopify-app-remix/apis/admin-api',
38
+ },
39
+ {
40
+ name: 'Manage Fulfillments',
41
+ subtitle: 'Receive fulfillment requests and cancellations.',
42
+ url: '/docs/apps/fulfillment/fulfillment-service-apps/manage-fulfillments',
43
+ type: 'shopify',
44
+ },
45
+ ],
46
+ };
47
+
48
+ export default data;
@@ -0,0 +1,82 @@
1
+ import {ShopifyHeader} from '@shopify/shopify-api';
2
+
3
+ import {adminClientFactory} from '../../clients/admin';
4
+ import {BasicParams} from '../../types';
5
+ import {ensureValidOfflineSession} from '../../helpers';
6
+
7
+ import type {
8
+ AuthenticateFulfillmentService,
9
+ FulfillmentServiceContext,
10
+ } from './types';
11
+
12
+ export function authenticateFulfillmentServiceFactory(
13
+ params: BasicParams,
14
+ ): AuthenticateFulfillmentService {
15
+ const {api, logger} = params;
16
+
17
+ return async function authenticate(
18
+ request: Request,
19
+ ): Promise<FulfillmentServiceContext> {
20
+ logger.info('Authenticating fulfillment service request');
21
+
22
+ if (request.method !== 'POST') {
23
+ logger.debug(
24
+ 'Received a non-POST request for fulfillment service. 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
+ const result = await api.fulfillmentService.validate({
35
+ rawBody,
36
+ rawRequest: request,
37
+ });
38
+
39
+ if (!result.valid) {
40
+ logger.error('Received an invalid fulfillment service request', {
41
+ reason: result.reason,
42
+ });
43
+
44
+ throw new Response(undefined, {
45
+ status: 400,
46
+ statusText: 'Bad Request',
47
+ });
48
+ }
49
+
50
+ const payload = JSON.parse(rawBody);
51
+ const shop = request.headers.get(ShopifyHeader.Domain) || '';
52
+
53
+ logger.debug(
54
+ 'Fulfillment service request is valid, looking for an offline session',
55
+ {
56
+ shop,
57
+ },
58
+ );
59
+
60
+ const session = await ensureValidOfflineSession(params, shop);
61
+
62
+ if (!session) {
63
+ logger.info('Fulfillment service request could not find session', {
64
+ shop,
65
+ });
66
+ throw new Response(undefined, {
67
+ status: 400,
68
+ statusText: 'Bad Request',
69
+ });
70
+ }
71
+
72
+ logger.debug('Found a session for the fulfillment service request', {
73
+ shop,
74
+ });
75
+
76
+ return {
77
+ session,
78
+ payload,
79
+ admin: adminClientFactory({params, session}),
80
+ };
81
+ };
82
+ }
@@ -0,0 +1,82 @@
1
+ import {Session} from '@shopify/shopify-api';
2
+
3
+ import type {AdminApiContext} from '../../clients';
4
+
5
+ export type FulfillmentServicePayload = Record<string, any> & {
6
+ kind: string;
7
+ };
8
+
9
+ export interface FulfillmentServiceContext {
10
+ /**
11
+ * A session with an offline token for the shop.
12
+ *
13
+ * Returned only if there is a session for the shop.
14
+ * @example
15
+ * <caption>Shopify session for the fulfillment service notification request.</caption>
16
+ * <description>Use the session associated with this request.</description>
17
+ * ```ts
18
+ * // /app/routes/fulfillment_service_notification.tsx
19
+ * import { ActionFunctionArgs } from "@remix-run/node";
20
+ * import { authenticate } from "../shopify.server";
21
+ *
22
+ * export const action = async ({ request }: ActionFunctionArgs) => {
23
+ * const { session, admin } = await authenticate.fulfillmentService(request);
24
+ *
25
+ * console.log(session.id)
26
+ *
27
+ * return new Response();
28
+ * };
29
+ * ```
30
+ * */
31
+ session: Session;
32
+ /**
33
+ *
34
+ * An admin context for the fulfillment service request.
35
+ *
36
+ * Returned only if there is a session for the shop.
37
+ * @example
38
+ * <caption>Shopify session for the fulfillment service request.</caption>
39
+ * <description>Use the session associated with this request to use the Admin GraphQL API </description>
40
+ * ```ts
41
+ * // /app/routes/fulfillment_order_notification.ts
42
+ * import { ActionFunctionArgs } from "@remix-run/node";
43
+ * import { authenticate } from "../shopify.server";
44
+ *
45
+ * export async function action({ request }: ActionFunctionArgs) {
46
+ * const { admin, session } = await authenticate.fulfillmentService(request);
47
+ *
48
+ * console.log(session.id)
49
+ *
50
+ * return new Response();
51
+ * }
52
+ * ```
53
+ */
54
+ admin: AdminApiContext;
55
+
56
+ /**
57
+ * The payload from the fulfillment service request.
58
+ *
59
+ * @example
60
+ * <caption>Fulfillment service request payload.</caption>
61
+ * <description>Get the request's POST payload.</description>
62
+ * ```ts
63
+ * /app/routes/fulfillment_order_notification.ts
64
+ * import { ActionFunction } from "@remix-run/node";
65
+ * import { authenticate } from "../shopify.server";
66
+ *
67
+ * export const action: ActionFunction = async ({ request }) => {
68
+ * const { payload } = await authenticate.fulfillmentService(request);
69
+ * if(payload.kind === 'FULFILLMENT_REQUEST') {
70
+ * // handle fulfillment request
71
+ * } else if (payload.kind === 'CANCELLATION_REQUEST') {
72
+ * // handle cancellation request
73
+ * };
74
+ * return new Response();
75
+ * ```
76
+ */
77
+ payload: FulfillmentServicePayload;
78
+ }
79
+
80
+ export type AuthenticateFulfillmentService = (
81
+ request: Request,
82
+ ) => Promise<FulfillmentServiceContext>;
@@ -0,0 +1,43 @@
1
+ import type {BasicParams} from '../../types';
2
+
3
+ export type AddDocumentResponseHeadersFunction = (
4
+ request: Request,
5
+ headers: Headers,
6
+ ) => void;
7
+
8
+ export function addDocumentResponseHeadersFactory(
9
+ params: BasicParams,
10
+ ): AddDocumentResponseHeadersFunction {
11
+ const {api, config} = params;
12
+
13
+ return function (request: Request, headers: Headers) {
14
+ const {searchParams} = new URL(request.url);
15
+ const shop = api.utils.sanitizeShop(searchParams.get('shop')!);
16
+
17
+ addDocumentResponseHeaders(headers, config.isEmbeddedApp, shop);
18
+ };
19
+ }
20
+
21
+ export function addDocumentResponseHeaders(
22
+ headers: Headers,
23
+ isEmbeddedApp: boolean,
24
+ shop: string | null | undefined,
25
+ ) {
26
+ if (shop) {
27
+ headers.set(
28
+ 'Link',
29
+ '<https://cdn.shopify.com/shopifycloud/app-bridge.js>; rel="preload"; as="script";',
30
+ );
31
+ }
32
+
33
+ if (isEmbeddedApp) {
34
+ if (shop) {
35
+ headers.set(
36
+ 'Content-Security-Policy',
37
+ `frame-ancestors https://${shop} https://admin.shopify.com https://*.spin.dev https://admin.myshopify.io https://admin.shop.dev;`,
38
+ );
39
+ }
40
+ } else {
41
+ headers.set('Content-Security-Policy', `frame-ancestors 'none';`);
42
+ }
43
+ }
@@ -0,0 +1,10 @@
1
+ import {APP_BRIDGE_URL} from '../const';
2
+
3
+ let appBridgeUrlOverride: string | undefined;
4
+ export function setAppBridgeUrlOverride(url: string) {
5
+ appBridgeUrlOverride = url;
6
+ }
7
+
8
+ export function appBridgeUrl() {
9
+ return appBridgeUrlOverride || APP_BRIDGE_URL;
10
+ }
@@ -0,0 +1,38 @@
1
+ import {BasicParams} from '../../types';
2
+ import {REAUTH_URL_HEADER} from '../const';
3
+
4
+ export interface EnsureCORSFunction {
5
+ (response: Response): Response;
6
+ }
7
+
8
+ export function ensureCORSHeadersFactory(
9
+ params: BasicParams,
10
+ request: Request,
11
+ corsHeaders: string[] = [],
12
+ ): EnsureCORSFunction {
13
+ const {logger, config} = params;
14
+
15
+ return function ensureCORSHeaders(response) {
16
+ const origin = request.headers.get('Origin');
17
+ if (origin && origin !== config.appUrl) {
18
+ logger.debug(
19
+ 'Request comes from a different origin, adding CORS headers',
20
+ );
21
+
22
+ const corsHeadersSet = new Set([
23
+ 'Authorization',
24
+ 'Content-Type',
25
+ ...corsHeaders,
26
+ ]);
27
+
28
+ response.headers.set('Access-Control-Allow-Origin', '*');
29
+ response.headers.set(
30
+ 'Access-Control-Allow-Headers',
31
+ [...corsHeadersSet].join(', '),
32
+ );
33
+ response.headers.set('Access-Control-Expose-Headers', REAUTH_URL_HEADER);
34
+ }
35
+
36
+ return response;
37
+ };
38
+ }
@@ -0,0 +1,11 @@
1
+ const SESSION_TOKEN_PARAM = 'id_token';
2
+
3
+ export function getSessionTokenHeader(request: Request): string | undefined {
4
+ return request.headers.get('authorization')?.replace('Bearer ', '');
5
+ }
6
+
7
+ export function getSessionTokenFromUrlParam(request: Request): string | null {
8
+ const url = new URL(request.url);
9
+
10
+ return url.searchParams.get(SESSION_TOKEN_PARAM);
11
+ }
@@ -0,0 +1,4 @@
1
+ export function getShopFromRequest(request: Request) {
2
+ const url = new URL(request.url);
3
+ return url.searchParams.get('shop')!;
4
+ }
@@ -0,0 +1,45 @@
1
+ export interface IdempotentHandlePromiseParams {
2
+ promiseFunction: () => Promise<any>;
3
+ identifier: string;
4
+ }
5
+
6
+ const IDENTIFIER_TTL_MS = 60000;
7
+
8
+ export class IdempotentPromiseHandler {
9
+ protected identifiers: Map<string, number>;
10
+
11
+ constructor() {
12
+ this.identifiers = new Map<string, number>();
13
+ }
14
+
15
+ async handlePromise({
16
+ promiseFunction,
17
+ identifier,
18
+ }: IdempotentHandlePromiseParams): Promise<any> {
19
+ try {
20
+ if (this.isPromiseRunnable(identifier)) {
21
+ await promiseFunction();
22
+ }
23
+ } finally {
24
+ this.clearStaleIdentifiers();
25
+ }
26
+
27
+ return Promise.resolve();
28
+ }
29
+
30
+ private isPromiseRunnable(identifier: string) {
31
+ if (!this.identifiers.has(identifier)) {
32
+ this.identifiers.set(identifier, Date.now());
33
+ return true;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ private async clearStaleIdentifiers() {
39
+ this.identifiers.forEach((date, identifier, map) => {
40
+ if (Date.now() - date > IDENTIFIER_TTL_MS) {
41
+ map.delete(identifier);
42
+ }
43
+ });
44
+ }
45
+ }
@@ -0,0 +1,10 @@
1
+ export * from './app-bridge-url';
2
+ export * from './add-response-headers';
3
+ export * from './ensure-cors-headers';
4
+ export * from './validate-session-token';
5
+ export * from './get-session-token-header';
6
+ export * from './invalidate-access-token';
7
+ export * from './reject-bot-request';
8
+ export * from './respond-to-options-request';
9
+ export * from './respond-to-invalid-session-token';
10
+ export * from './get-shop-from-request';
@@ -0,0 +1,17 @@
1
+ import {Session} from '@shopify/shopify-api';
2
+
3
+ import type {BasicParams} from '../../types';
4
+
5
+ export async function invalidateAccessToken(
6
+ params: BasicParams,
7
+ session: Session,
8
+ ): Promise<void> {
9
+ const {logger, config} = params;
10
+
11
+ logger.debug(`Invalidating access token for session - ${session.id}`, {
12
+ shop: session.shop,
13
+ });
14
+
15
+ session.accessToken = undefined;
16
+ await config.sessionStorage!.storeSession(session);
17
+ }
@@ -0,0 +1,27 @@
1
+ import {isbot} from 'isbot';
2
+
3
+ import type {BasicParams} from '../../types';
4
+
5
+ const SHOPIFY_POS_USER_AGENT = /Shopify POS\//;
6
+ const SHOPIFY_MOBILE_USER_AGENT = /Shopify Mobile\//;
7
+
8
+ const SHOPIFY_USER_AGENTS = [SHOPIFY_POS_USER_AGENT, SHOPIFY_MOBILE_USER_AGENT];
9
+
10
+ export function respondToBotRequest(
11
+ {logger}: BasicParams,
12
+ request: Request,
13
+ ): void | never {
14
+ const userAgent = request.headers.get('User-Agent') ?? '';
15
+
16
+ // We call isbot below to prevent good (self-identifying) bots from triggering auth requests, but there are some
17
+ // Shopify-specific cases we want to allow that are identified as bots by isbot.
18
+ if (SHOPIFY_USER_AGENTS.some((agent) => agent.test(userAgent))) {
19
+ logger.debug('Request is from a Shopify agent, allow');
20
+ return;
21
+ }
22
+
23
+ if (isbot(userAgent)) {
24
+ logger.debug('Request is from a bot, skipping auth');
25
+ throw new Response(undefined, {status: 410, statusText: 'Gone'});
26
+ }
27
+ }
@@ -0,0 +1,28 @@
1
+ import {redirectToBouncePage} from '../admin/helpers/redirect-to-bounce-page';
2
+ import {RETRY_INVALID_SESSION_HEADER} from '../const';
3
+ import {BasicParams} from '../../types';
4
+
5
+ interface RespondToInvalidSessionTokenParams {
6
+ params: BasicParams;
7
+ request: Request;
8
+ retryRequest?: boolean;
9
+ }
10
+
11
+ export function respondToInvalidSessionToken({
12
+ params,
13
+ request,
14
+ retryRequest = false,
15
+ }: RespondToInvalidSessionTokenParams) {
16
+ const {api, logger, config} = params;
17
+
18
+ const isDocumentRequest = !request.headers.get('authorization');
19
+ if (isDocumentRequest) {
20
+ return redirectToBouncePage({api, logger, config}, new URL(request.url));
21
+ }
22
+
23
+ throw new Response(undefined, {
24
+ status: 401,
25
+ statusText: 'Unauthorized',
26
+ headers: retryRequest ? RETRY_INVALID_SESSION_HEADER : {},
27
+ });
28
+ }
@@ -0,0 +1,26 @@
1
+ import {BasicParams} from '../../types';
2
+
3
+ import {ensureCORSHeadersFactory} from './ensure-cors-headers';
4
+
5
+ export function respondToOptionsRequest(
6
+ params: BasicParams,
7
+ request: Request,
8
+ corsHeaders?: string[],
9
+ ) {
10
+ if (request.method === 'OPTIONS') {
11
+ const ensureCORSHeaders = ensureCORSHeadersFactory(
12
+ params,
13
+ request,
14
+ corsHeaders,
15
+ );
16
+
17
+ throw ensureCORSHeaders(
18
+ new Response(null, {
19
+ status: 204,
20
+ headers: {
21
+ 'Access-Control-Max-Age': '7200',
22
+ },
23
+ }),
24
+ );
25
+ }
26
+ }
@@ -0,0 +1,40 @@
1
+ import {JwtPayload} from '@shopify/shopify-api';
2
+
3
+ import type {BasicParams} from '../../types';
4
+
5
+ import {respondToInvalidSessionToken} from './respond-to-invalid-session-token';
6
+ import {getShopFromRequest} from './get-shop-from-request';
7
+
8
+ interface ValidateSessionTokenOptions {
9
+ checkAudience?: boolean;
10
+ retryRequest?: boolean;
11
+ }
12
+
13
+ export async function validateSessionToken(
14
+ params: BasicParams,
15
+ request: Request,
16
+ token: string,
17
+ {checkAudience = true, retryRequest = true}: ValidateSessionTokenOptions = {},
18
+ ): Promise<JwtPayload> {
19
+ const {api, logger} = params;
20
+ const shop = getShopFromRequest(request);
21
+ logger.debug('Validating session token', {shop});
22
+
23
+ try {
24
+ const payload = await api.session.decodeSessionToken(token, {
25
+ checkAudience,
26
+ });
27
+ logger.debug('Session token is valid - validated', {
28
+ shop,
29
+ payload: JSON.stringify(payload),
30
+ });
31
+
32
+ return payload;
33
+ } catch (error) {
34
+ logger.debug(`Failed to validate session token: ${error.message}`, {
35
+ shop,
36
+ });
37
+
38
+ throw respondToInvalidSessionToken({params, request, retryRequest});
39
+ }
40
+ }