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