@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.
- package/dist/cjs/react/components/AppProvider/AppProvider.js.map +1 -1
- package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/merchant-custom-app.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/token-exchange.js.map +1 -1
- package/dist/cjs/server/authenticate/webhooks/authenticate.js +34 -10
- package/dist/cjs/server/authenticate/webhooks/authenticate.js.map +1 -1
- package/dist/cjs/server/version.js +1 -1
- package/dist/cjs/server/version.js.map +1 -1
- package/dist/esm/react/components/AppProvider/AppProvider.mjs.map +1 -1
- package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/auth-code-flow.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/merchant-custom-app.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/token-exchange.mjs.map +1 -1
- package/dist/esm/server/authenticate/webhooks/authenticate.mjs +35 -11
- package/dist/esm/server/authenticate/webhooks/authenticate.mjs.map +1 -1
- package/dist/esm/server/version.mjs +1 -1
- package/dist/esm/server/version.mjs.map +1 -1
- package/dist/ts/react/components/AppProvider/AppProvider.d.ts.map +1 -1
- package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/merchant-custom-app.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/public/extension/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/webhooks/authenticate.d.ts.map +1 -1
- package/dist/ts/server/authenticate/webhooks/types.d.ts +34 -2
- package/dist/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
- package/dist/ts/server/config-types.d.ts.map +1 -1
- package/dist/ts/server/version.d.ts +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +18 -14
- package/src/react/components/AppProvider/AppProvider.doc.ts +35 -0
- package/src/react/components/AppProvider/AppProvider.tsx +147 -0
- package/src/react/components/AppProvider/index.ts +1 -0
- package/src/react/components/AppProxyForm/AppProxyForm.doc.ts +28 -0
- package/src/react/components/AppProxyForm/AppProxyForm.tsx +82 -0
- package/src/react/components/AppProxyForm/index.ts +1 -0
- package/src/react/components/AppProxyLink/AppProxyLink.doc.ts +28 -0
- package/src/react/components/AppProxyLink/AppProxyLink.tsx +58 -0
- package/src/react/components/AppProxyLink/index.ts +1 -0
- package/src/react/components/AppProxyProvider/AppProxyProvider.doc.ts +40 -0
- package/src/react/components/AppProxyProvider/AppProxyProvider.tsx +100 -0
- package/src/react/components/AppProxyProvider/index.ts +5 -0
- package/src/react/components/RemixPolarisLink.tsx +14 -0
- package/src/react/components/index.ts +4 -0
- package/src/react/const.ts +2 -0
- package/src/react/index.ts +1 -0
- package/src/server/__test-helpers/const.ts +14 -0
- package/src/server/__test-helpers/expect-admin-api-client.ts +51 -0
- package/src/server/__test-helpers/expect-begin-auth-redirect.ts +22 -0
- package/src/server/__test-helpers/expect-document-request-headers.ts +25 -0
- package/src/server/__test-helpers/expect-exit-iframe.ts +37 -0
- package/src/server/__test-helpers/expect-login-redirect.ts +8 -0
- package/src/server/__test-helpers/expect-storefront-api-client.ts +49 -0
- package/src/server/__test-helpers/expect-token-refresh.ts +188 -0
- package/src/server/__test-helpers/get-hmac.ts +7 -0
- package/src/server/__test-helpers/get-jwt.ts +10 -0
- package/src/server/__test-helpers/get-thrown-error.ts +19 -0
- package/src/server/__test-helpers/get-thrown-response.ts +17 -0
- package/src/server/__test-helpers/index.ts +21 -0
- package/src/server/__test-helpers/mock-graphql-request.ts +60 -0
- package/src/server/__test-helpers/request-mock.ts +169 -0
- package/src/server/__test-helpers/setup-embedded-flow.ts +30 -0
- package/src/server/__test-helpers/setup-fetch-flow.ts +29 -0
- package/src/server/__test-helpers/setup-non-embedded-flow.ts +31 -0
- package/src/server/__test-helpers/setup-valid-request.ts +78 -0
- package/src/server/__test-helpers/setup-valid-session.ts +28 -0
- package/src/server/__test-helpers/sign-request-cookie.ts +21 -0
- package/src/server/__test-helpers/test-config.ts +56 -0
- package/src/server/adapters/node/index.ts +13 -0
- package/src/server/adapters/vercel/index.ts +13 -0
- package/src/server/authenticate/admin/authenticate.admin.doc.example.ts +34 -0
- package/src/server/authenticate/admin/authenticate.admin.doc.ts +51 -0
- package/src/server/authenticate/admin/authenticate.ts +232 -0
- package/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts +29 -0
- package/src/server/authenticate/admin/billing/cancel.ts +38 -0
- package/src/server/authenticate/admin/billing/check.ts +41 -0
- package/src/server/authenticate/admin/billing/create-usage-record.ts +36 -0
- package/src/server/authenticate/admin/billing/helpers.ts +42 -0
- package/src/server/authenticate/admin/billing/index.ts +6 -0
- package/src/server/authenticate/admin/billing/request.ts +64 -0
- package/src/server/authenticate/admin/billing/require.ts +58 -0
- package/src/server/authenticate/admin/billing/types.ts +613 -0
- package/src/server/authenticate/admin/billing/update-usage-subscription-capped-amount.ts +55 -0
- package/src/server/authenticate/admin/helpers/begin-auth.ts +17 -0
- package/src/server/authenticate/admin/helpers/create-admin-api-context.ts +20 -0
- package/src/server/authenticate/admin/helpers/ensure-app-is-embedded-if-required.ts +18 -0
- package/src/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.ts +25 -0
- package/src/server/authenticate/admin/helpers/handle-client-error.ts +45 -0
- package/src/server/authenticate/admin/helpers/index.ts +14 -0
- package/src/server/authenticate/admin/helpers/redirect-to-auth-page.ts +28 -0
- package/src/server/authenticate/admin/helpers/redirect-to-bounce-page.ts +23 -0
- package/src/server/authenticate/admin/helpers/redirect-to-install-page.ts +53 -0
- package/src/server/authenticate/admin/helpers/redirect-to-shopify-or-app-root.ts +21 -0
- package/src/server/authenticate/admin/helpers/redirect-with-app-bridge-headers.ts +13 -0
- package/src/server/authenticate/admin/helpers/redirect-with-exitiframe.ts +28 -0
- package/src/server/authenticate/admin/helpers/redirect.ts +164 -0
- package/src/server/authenticate/admin/helpers/render-app-bridge.ts +46 -0
- package/src/server/authenticate/admin/helpers/trigger-after-auth-hook.ts +29 -0
- package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +67 -0
- package/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +46 -0
- package/src/server/authenticate/admin/scope/authenticate.admin.scopes.doc.ts +28 -0
- package/src/server/authenticate/admin/scope/client/fetch-scopes-details.ts +43 -0
- package/src/server/authenticate/admin/scope/client/revoke-scopes.ts +38 -0
- package/src/server/authenticate/admin/scope/factory.ts +21 -0
- package/src/server/authenticate/admin/scope/query.ts +51 -0
- package/src/server/authenticate/admin/scope/request.ts +32 -0
- package/src/server/authenticate/admin/scope/revoke.ts +48 -0
- package/src/server/authenticate/admin/scope/types.ts +126 -0
- package/src/server/authenticate/admin/strategies/auth-code-flow.ts +332 -0
- package/src/server/authenticate/admin/strategies/merchant-custom-app.ts +65 -0
- package/src/server/authenticate/admin/strategies/token-exchange.ts +188 -0
- package/src/server/authenticate/admin/strategies/types.ts +29 -0
- package/src/server/authenticate/admin/types.ts +216 -0
- package/src/server/authenticate/const.ts +15 -0
- package/src/server/authenticate/flow/authenticate.flow.doc.example.ts +39 -0
- package/src/server/authenticate/flow/authenticate.flow.doc.ts +47 -0
- package/src/server/authenticate/flow/authenticate.ts +68 -0
- package/src/server/authenticate/flow/types.ts +85 -0
- package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts +52 -0
- package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.ts +48 -0
- package/src/server/authenticate/fulfillment-service/authenticate.ts +82 -0
- package/src/server/authenticate/fulfillment-service/types.ts +82 -0
- package/src/server/authenticate/helpers/add-response-headers.ts +43 -0
- package/src/server/authenticate/helpers/app-bridge-url.ts +10 -0
- package/src/server/authenticate/helpers/ensure-cors-headers.ts +38 -0
- package/src/server/authenticate/helpers/get-session-token-header.ts +11 -0
- package/src/server/authenticate/helpers/get-shop-from-request.ts +4 -0
- package/src/server/authenticate/helpers/idempotent-promise-handler.ts +45 -0
- package/src/server/authenticate/helpers/index.ts +10 -0
- package/src/server/authenticate/helpers/invalidate-access-token.ts +17 -0
- package/src/server/authenticate/helpers/reject-bot-request.ts +27 -0
- package/src/server/authenticate/helpers/respond-to-invalid-session-token.ts +28 -0
- package/src/server/authenticate/helpers/respond-to-options-request.ts +26 -0
- package/src/server/authenticate/helpers/validate-session-token.ts +40 -0
- package/src/server/authenticate/login/login.ts +53 -0
- package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.example.ts +27 -0
- package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts +55 -0
- package/src/server/authenticate/public/appProxy/authenticate.ts +150 -0
- package/src/server/authenticate/public/appProxy/types.ts +215 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.offers.ts +21 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.ts +17 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.ts +49 -0
- package/src/server/authenticate/public/checkout/authenticate.ts +13 -0
- package/src/server/authenticate/public/checkout/types.ts +65 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.offers.ts +36 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.ts +18 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.ts +49 -0
- package/src/server/authenticate/public/customer-account/authenticate.ts +13 -0
- package/src/server/authenticate/public/customer-account/types.ts +65 -0
- package/src/server/authenticate/public/extension/authenticate.ts +54 -0
- package/src/server/authenticate/public/extension/types.ts +11 -0
- package/src/server/authenticate/public/factory.ts +24 -0
- package/src/server/authenticate/public/index.ts +1 -0
- package/src/server/authenticate/public/pos/authenticate.ts +8 -0
- package/src/server/authenticate/public/pos/types.ts +65 -0
- package/src/server/authenticate/public/types.ts +96 -0
- package/src/server/authenticate/webhooks/authenticate.ts +105 -0
- package/src/server/authenticate/webhooks/authenticate.webhooks.doc.example.ts +43 -0
- package/src/server/authenticate/webhooks/authenticate.webhooks.doc.ts +39 -0
- package/src/server/authenticate/webhooks/index.ts +1 -0
- package/src/server/authenticate/webhooks/register.ts +48 -0
- package/src/server/authenticate/webhooks/types.ts +266 -0
- package/src/server/boundary/error.tsx +14 -0
- package/src/server/boundary/headers.ts +15 -0
- package/src/server/boundary/index.ts +39 -0
- package/src/server/boundary/types.ts +4 -0
- package/src/server/clients/admin/authenticate.admin.api.doc.ts +34 -0
- package/src/server/clients/admin/factory.ts +22 -0
- package/src/server/clients/admin/graphql.ts +39 -0
- package/src/server/clients/admin/index.ts +2 -0
- package/src/server/clients/admin/types.ts +120 -0
- package/src/server/clients/index.ts +2 -0
- package/src/server/clients/storefront/authenticate.storefront.api.doc.ts +34 -0
- package/src/server/clients/storefront/factory.ts +32 -0
- package/src/server/clients/storefront/index.ts +2 -0
- package/src/server/clients/storefront/types.ts +78 -0
- package/src/server/clients/types.ts +47 -0
- package/src/server/config-types.ts +309 -0
- package/src/server/errors.ts +3 -0
- package/src/server/future/flags.ts +62 -0
- package/src/server/helpers/create-or-load-offline-session.ts +19 -0
- package/src/server/helpers/ensure-offline-token-is-not-expired.ts +32 -0
- package/src/server/helpers/ensure-valid-offline-session.ts +15 -0
- package/src/server/helpers/index.ts +4 -0
- package/src/server/helpers/refresh-token.ts +35 -0
- package/src/server/index.ts +24 -0
- package/src/server/override-logger.ts +42 -0
- package/src/server/shopify-app.doc.ts +45 -0
- package/src/server/shopify-app.ts +215 -0
- package/src/server/test-helpers/index.ts +1 -0
- package/src/server/test-helpers/test-config.ts +79 -0
- package/src/server/types-contexts.ts +44 -0
- package/src/server/types.ts +507 -0
- package/src/server/unauthenticated/admin/factory.ts +23 -0
- package/src/server/unauthenticated/admin/index.ts +1 -0
- package/src/server/unauthenticated/admin/types.ts +79 -0
- package/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts +40 -0
- package/src/server/unauthenticated/storefront/factory.ts +28 -0
- package/src/server/unauthenticated/storefront/index.ts +1 -0
- package/src/server/unauthenticated/storefront/types.ts +105 -0
- package/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts +40 -0
- package/src/server/unauthenticated/types.ts +65 -0
- 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>;
|
package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts
ADDED
|
@@ -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,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
|
+
}
|