@shopify/shopify-app-remix 1.0.0-rc.1 → 1.0.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/README.md +73 -18
- package/build/cjs/adapters/node/index.js +10 -0
- package/build/cjs/auth/admin/authenticate.js +25 -30
- package/build/cjs/auth/const.js +0 -8
- package/build/cjs/auth/helpers/add-response-headers.js +13 -50
- package/build/cjs/auth/helpers/app-bridge-url.js +16 -0
- package/build/cjs/auth/helpers/ensure-cors-headers.js +24 -0
- package/build/cjs/auth/helpers/redirect-with-app-bridge-headers.js +3 -9
- package/build/cjs/auth/helpers/respond-to-options-request.js +19 -0
- package/build/cjs/auth/login/login.js +0 -6
- package/build/cjs/auth/public/authenticate.js +5 -1
- package/build/cjs/billing/request.js +1 -1
- package/build/cjs/boundary/error.js +12 -0
- package/build/cjs/boundary/headers.js +18 -0
- package/build/cjs/index.js +57 -8
- package/build/cjs/types.js +1 -0
- package/build/cjs/version.js +1 -1
- package/build/ts/adapters/node/index.js +6 -0
- package/build/ts/adapters/node/index.js.map +1 -1
- package/build/ts/auth/admin/authenticate.d.ts +0 -2
- package/build/ts/auth/admin/authenticate.d.ts.map +1 -1
- package/build/ts/auth/admin/authenticate.js +13 -25
- package/build/ts/auth/admin/authenticate.js.map +1 -1
- package/build/ts/auth/admin/types.d.ts +32 -13
- package/build/ts/auth/admin/types.d.ts.map +1 -1
- package/build/ts/auth/const.d.ts +1 -2
- package/build/ts/auth/const.d.ts.map +1 -1
- package/build/ts/auth/const.js +2 -3
- package/build/ts/auth/const.js.map +1 -1
- package/build/ts/auth/helpers/add-response-headers.d.ts +2 -3
- package/build/ts/auth/helpers/add-response-headers.d.ts.map +1 -1
- package/build/ts/auth/helpers/add-response-headers.js +16 -49
- package/build/ts/auth/helpers/add-response-headers.js.map +1 -1
- package/build/ts/auth/helpers/app-bridge-url.d.ts +3 -0
- package/build/ts/auth/helpers/app-bridge-url.d.ts.map +1 -0
- package/build/ts/auth/helpers/app-bridge-url.js +14 -0
- package/build/ts/auth/helpers/app-bridge-url.js.map +1 -0
- package/build/ts/auth/helpers/ensure-cors-headers.d.ts +6 -0
- package/build/ts/auth/helpers/ensure-cors-headers.d.ts.map +1 -0
- package/build/ts/auth/helpers/ensure-cors-headers.js +19 -0
- package/build/ts/auth/helpers/ensure-cors-headers.js.map +1 -0
- package/build/ts/auth/helpers/index.d.ts +11 -7
- package/build/ts/auth/helpers/index.d.ts.map +1 -1
- package/build/ts/auth/helpers/index.js +12 -15
- package/build/ts/auth/helpers/index.js.map +1 -1
- package/build/ts/auth/helpers/redirect-with-app-bridge-headers.d.ts +1 -1
- package/build/ts/auth/helpers/redirect-with-app-bridge-headers.d.ts.map +1 -1
- package/build/ts/auth/helpers/redirect-with-app-bridge-headers.js +3 -7
- package/build/ts/auth/helpers/redirect-with-app-bridge-headers.js.map +1 -1
- package/build/ts/auth/helpers/respond-to-options-request.d.ts +3 -0
- package/build/ts/auth/helpers/respond-to-options-request.d.ts.map +1 -0
- package/build/ts/auth/helpers/respond-to-options-request.js +17 -0
- package/build/ts/auth/helpers/respond-to-options-request.js.map +1 -0
- package/build/ts/auth/login/login.d.ts.map +1 -1
- package/build/ts/auth/login/login.js +0 -4
- package/build/ts/auth/login/login.js.map +1 -1
- package/build/ts/auth/public/authenticate.d.ts.map +1 -1
- package/build/ts/auth/public/authenticate.js +2 -0
- package/build/ts/auth/public/authenticate.js.map +1 -1
- package/build/ts/auth/public/types.d.ts +25 -2
- package/build/ts/auth/public/types.d.ts.map +1 -1
- package/build/ts/billing/request.js +1 -1
- package/build/ts/billing/request.js.map +1 -1
- package/build/ts/billing/types.d.ts +20 -12
- package/build/ts/billing/types.d.ts.map +1 -1
- package/build/ts/boundary/error.d.ts +2 -0
- package/build/ts/boundary/error.d.ts.map +1 -0
- package/build/ts/boundary/error.js +11 -0
- package/build/ts/boundary/error.js.map +1 -0
- package/build/ts/boundary/headers.d.ts +3 -0
- package/build/ts/boundary/headers.d.ts.map +1 -0
- package/build/ts/boundary/headers.js +13 -0
- package/build/ts/boundary/headers.js.map +1 -0
- package/build/ts/boundary/types.d.ts +4 -0
- package/build/ts/boundary/types.d.ts.map +1 -0
- package/build/ts/boundary/types.js +3 -0
- package/build/ts/boundary/types.js.map +1 -0
- package/build/ts/config-types.d.ts +28 -18
- package/build/ts/config-types.d.ts.map +1 -1
- package/build/ts/index.d.ts +39 -1
- package/build/ts/index.d.ts.map +1 -1
- package/build/ts/index.js +56 -10
- package/build/ts/index.js.map +1 -1
- package/build/ts/types.d.ts +88 -122
- package/build/ts/types.d.ts.map +1 -1
- package/build/ts/types.js.map +1 -1
- package/build/ts/version.d.ts +1 -1
- package/build/ts/version.d.ts.map +1 -1
- package/build/ts/version.js +1 -1
- package/build/ts/version.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,13 +38,14 @@ Create `app/shopify.server.js`. We will use this file to configure our Shopify a
|
|
|
38
38
|
import '@shopify/shopify-app-remix/adapters/node';
|
|
39
39
|
import {LATEST_API_VERSION, shopifyApp} from '@shopify/shopify-app-remix';
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const shopify = shopifyApp({
|
|
42
42
|
apiKey: process.env.SHOPIFY_API_KEY!,
|
|
43
43
|
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
|
|
44
44
|
appUrl: process.env.SHOPIFY_APP_URL!,
|
|
45
45
|
scopes: ['read_products'],
|
|
46
46
|
apiVersion: LATEST_API_VERSION,
|
|
47
47
|
});
|
|
48
|
+
export default shopify;
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
A description of these config options:
|
|
@@ -72,7 +73,7 @@ SHOPIFY_APP_URL="[The tunnel URL you are using to run your app]"
|
|
|
72
73
|
// app/shopify.server.js
|
|
73
74
|
import {shopifyApp} from '@shopify/shopify-app-remix';
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
const shopify = shopifyApp({
|
|
76
77
|
// ...
|
|
77
78
|
authPathPrefix: '/auth',
|
|
78
79
|
});
|
|
@@ -84,7 +85,7 @@ Now let's create the [splat route](https://remix.run/docs/en/main/guides/routing
|
|
|
84
85
|
// app/routes/auth/$.tsx
|
|
85
86
|
import {LoaderArgs} from '@remix-run/node';
|
|
86
87
|
|
|
87
|
-
import
|
|
88
|
+
import shopify from '../../shopify.server';
|
|
88
89
|
|
|
89
90
|
export async function loader({request}: LoaderArgs) {
|
|
90
91
|
await shopify.authenticate.admin(request);
|
|
@@ -128,14 +129,14 @@ export default function App() {
|
|
|
128
129
|
> **Note**: This version of App Bridge must be loaded from the CDN, in the document head.
|
|
129
130
|
|
|
130
131
|
Now that your app is ready to respond to requests, it will also need to add the required `Content-Security-Policy` header directives, as per [our documentation](https://shopify.dev/docs/apps/store/security/iframe-protection).
|
|
131
|
-
To do that, this package provides the `shopify.
|
|
132
|
+
To do that, this package provides the `shopify.addDocumentResponseHeaders` method.
|
|
132
133
|
|
|
133
134
|
You should return these headers from any endpoint that renders HTML in your app.
|
|
134
135
|
Most likely you'll want to add this to every HTML response by updating the entry.server.tsx file:
|
|
135
136
|
|
|
136
137
|
```ts
|
|
137
138
|
// entry.server.tsx
|
|
138
|
-
import
|
|
139
|
+
import shopify from './shopify.server';
|
|
139
140
|
|
|
140
141
|
export default function handleRequest(
|
|
141
142
|
request: Request,
|
|
@@ -148,7 +149,7 @@ export default function handleRequest(
|
|
|
148
149
|
);
|
|
149
150
|
|
|
150
151
|
responseHeaders.set('Content-Type', 'text/html');
|
|
151
|
-
shopify.
|
|
152
|
+
shopify.addDocumentResponseHeaders(request, responseHeaders);
|
|
152
153
|
|
|
153
154
|
return new Response('<!DOCTYPE html>' + markup, {
|
|
154
155
|
status: responseStatusCode,
|
|
@@ -184,7 +185,8 @@ To load your app within the Shopify Admin, you need to:
|
|
|
184
185
|
|
|
185
186
|
## Authenticating admin requests
|
|
186
187
|
|
|
187
|
-
`shopifyApp` provides methods for authenticating admin requests.
|
|
188
|
+
`shopifyApp` provides methods for authenticating admin requests.
|
|
189
|
+
To authenticate admin requests you can call `shopify.authenticate.admin(request)` in a loader or an action:
|
|
188
190
|
|
|
189
191
|
```ts
|
|
190
192
|
// app/routes/**/*.tsx
|
|
@@ -195,7 +197,44 @@ export const loader = async ({request}: LoaderArgs) => {
|
|
|
195
197
|
};
|
|
196
198
|
```
|
|
197
199
|
|
|
198
|
-
If there is a session for this user, this loader will return null.
|
|
200
|
+
If there is a session for this user, this loader will return null.
|
|
201
|
+
If there is no session for the user, the loader will throw the appropriate redirect Response.
|
|
202
|
+
|
|
203
|
+
> **Note**: If you are authenticating more than one route, we recommend using [Remix layout routes](https://remix.run/docs/en/1.18.1/file-conventions/routes-files#layout-routes) to automatically authenticate them.
|
|
204
|
+
|
|
205
|
+
## Authenticating cross-origin admin requests
|
|
206
|
+
|
|
207
|
+
If your Remix server is authenticating an admin extension, a request from the extension to Remix will be cross-origin. Here `shopify.authenticate.admin` provides a cors function to add the required cross-origin headers:
|
|
208
|
+
|
|
209
|
+
````ts
|
|
210
|
+
export const loader = async ({request}: LoaderArgs) => {
|
|
211
|
+
const {cors} = await shopify.authenticate.admin(request);
|
|
212
|
+
|
|
213
|
+
return cors(json({"my": "data"}));
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Headers
|
|
218
|
+
|
|
219
|
+
It's important to note that the authentication functions in this package rely on throwing `Response` objects, which must be handled in your Remix routes using them.
|
|
220
|
+
|
|
221
|
+
To do that, you can set up a [Remix `ErrorBoundary`](https://remix.run/docs/en/main/guides/errors).
|
|
222
|
+
We provide some abstractions for the error and headers boundaries to make it easier for apps to set those up.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// app/routes/**/*.tsx
|
|
226
|
+
import {boundary} from '@shopify/shopify-app-remix';
|
|
227
|
+
|
|
228
|
+
export function ErrorBoundary() {
|
|
229
|
+
return boundary.error(useRouteError());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const headers = (headersArgs) => {
|
|
233
|
+
return boundary.headers(headersArgs);
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
> **Note**: You can also add this to a layout if you want to authenticate more than one route.
|
|
199
238
|
|
|
200
239
|
### Using the Shopify admin GraphQL API
|
|
201
240
|
|
|
@@ -205,7 +244,7 @@ E.g:
|
|
|
205
244
|
|
|
206
245
|
```ts
|
|
207
246
|
// routes/**/*.tsx
|
|
208
|
-
import
|
|
247
|
+
import shopify from '../shopify.server';
|
|
209
248
|
import {ActionArgs, json} from '@remix-run/node';
|
|
210
249
|
|
|
211
250
|
export async function action({request}: ActionArgs) {
|
|
@@ -243,7 +282,7 @@ export async function action({request}: ActionArgs) {
|
|
|
243
282
|
// app/routes/**/*.tsx
|
|
244
283
|
import {restResources} from '@shopify/shopify-api/rest/admin/2023-04';
|
|
245
284
|
|
|
246
|
-
|
|
285
|
+
const shopify = shopifyApp({
|
|
247
286
|
restResources,
|
|
248
287
|
// ...etc
|
|
249
288
|
});
|
|
@@ -274,7 +313,7 @@ To setup webhooks first we need to configure `shopifyApp` with 2 pieces:
|
|
|
274
313
|
// shopify.server.js
|
|
275
314
|
import {shopifyApp, DeliveryMethod} from '@shopify/shopify-app-remix';
|
|
276
315
|
|
|
277
|
-
|
|
316
|
+
const shopify = shopifyApp({
|
|
278
317
|
apiKey: '1707326264fde5037c658n120626ce3f',
|
|
279
318
|
// ...etc
|
|
280
319
|
webhooks: {
|
|
@@ -299,7 +338,7 @@ To do this, your app must authenticate the request.
|
|
|
299
338
|
// routes/webhooks.tsx
|
|
300
339
|
import {ActionArgs} from '@remix-run/node';
|
|
301
340
|
|
|
302
|
-
import
|
|
341
|
+
import shopify from '../shopify.server';
|
|
303
342
|
import db from '../db.server';
|
|
304
343
|
|
|
305
344
|
export const action = async ({request}: ActionArgs) => {
|
|
@@ -325,24 +364,28 @@ export const action = async ({request}: ActionArgs) => {
|
|
|
325
364
|
Your Remix app may need to authenticate requests coming from a public context. An example of this would be a checkout extension. Here is how:
|
|
326
365
|
|
|
327
366
|
```ts
|
|
328
|
-
// e.g: routes/api
|
|
329
|
-
import
|
|
367
|
+
// e.g: routes/api.public.notes.tsx
|
|
368
|
+
import shopify from '../shopify.server';
|
|
330
369
|
import {LoaderArgs, json} from '@remix-run/node';
|
|
331
370
|
import {getNotes} from '~/models/notes';
|
|
332
371
|
|
|
333
372
|
export const loader = async ({request}: LoaderArgs) => {
|
|
334
|
-
const {sessionToken} = await shopify.authenticate.public(request);
|
|
373
|
+
const {sessionToken, cors} = await shopify.authenticate.public(request);
|
|
335
374
|
|
|
336
375
|
// E.g: Get notes using the shops admin domain
|
|
337
|
-
return json(await getNotes(sessionToken.iss));
|
|
376
|
+
return cors(json(await getNotes(sessionToken.iss)));
|
|
338
377
|
};
|
|
339
378
|
```
|
|
340
379
|
|
|
341
380
|
This can be useful if your app exposes checkout or theme extensions and those extensions need to access data from your app.
|
|
342
381
|
|
|
382
|
+
**Note:** These requests will be cross-origin. So you must use the cross-origin helper returned from `shopify.authenticate.public`.
|
|
383
|
+
|
|
343
384
|
## Session Storage
|
|
344
385
|
|
|
345
|
-
|
|
386
|
+
When calling `shopifyApp`, you must pass in a `sesionStorage` to store sessions.
|
|
387
|
+
You can change this by passing a different Session Adaptor to `shopifyApp`.
|
|
388
|
+
To make this easy Shopify offers [some production ready session adaptors](https://github.com/Shopify/shopify-app-js/tree/main/packages).
|
|
346
389
|
|
|
347
390
|
In this example we'll swap the default session adaptor for [Prisma](https://www.prisma.io/).
|
|
348
391
|
|
|
@@ -363,4 +406,16 @@ const shopify = shopifyApp({
|
|
|
363
406
|
});
|
|
364
407
|
```
|
|
365
408
|
|
|
366
|
-
Note that this requires a `schema.prisma` file as defined in the README for [Prisma app session storage](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-session-storage-prisma)
|
|
409
|
+
Note that this requires a `schema.prisma` file as defined in the README for [Prisma app session storage](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-session-storage-prisma).
|
|
410
|
+
Remember to [set up your migrations](https://www.prisma.io/docs/concepts/components/prisma-migrate/get-started) after creating the schema file!
|
|
411
|
+
|
|
412
|
+
## Gotchas / Troubleshooting
|
|
413
|
+
|
|
414
|
+
### Navigating to other pages breaks
|
|
415
|
+
|
|
416
|
+
In Remix apps, you can navigate to a different page either by adding an `<a>` tag, or using the `<Link>` component from `@remix-run/react`.
|
|
417
|
+
|
|
418
|
+
In Shopify Remix apps you should avoid using `<a>`.
|
|
419
|
+
Use `<Link> `from `@remix-run/react` instead.
|
|
420
|
+
This ensures that your user remains authenticated.
|
|
421
|
+
````
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
var runtime = require('@shopify/shopify-api/runtime');
|
|
5
|
+
var appBridgeUrl = require('../../auth/helpers/app-bridge-url.js');
|
|
6
|
+
require('@remix-run/server-runtime');
|
|
7
|
+
require('isbot');
|
|
8
|
+
require('@shopify/shopify-api');
|
|
5
9
|
|
|
6
10
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
7
11
|
|
|
@@ -11,3 +15,9 @@ runtime.setCrypto(crypto__default["default"]);
|
|
|
11
15
|
runtime.setAbstractRuntimeString(() => {
|
|
12
16
|
return `Remix (Node)`;
|
|
13
17
|
});
|
|
18
|
+
|
|
19
|
+
/* eslint-disable no-process-env */
|
|
20
|
+
if (process.env.APP_BRIDGE_URL) {
|
|
21
|
+
appBridgeUrl.setAppBridgeUrlOverride(process.env.APP_BRIDGE_URL);
|
|
22
|
+
}
|
|
23
|
+
/* eslint-enable no-process-env */
|
|
@@ -7,14 +7,16 @@ var shopifyApi = require('@shopify/shopify-api');
|
|
|
7
7
|
var cancel = require('../../billing/cancel.js');
|
|
8
8
|
var require$1 = require('../../billing/require.js');
|
|
9
9
|
var request = require('../../billing/request.js');
|
|
10
|
+
var appBridgeUrl = require('../helpers/app-bridge-url.js');
|
|
10
11
|
var addResponseHeaders = require('../helpers/add-response-headers.js');
|
|
11
12
|
var redirectToAuthPage = require('../helpers/redirect-to-auth-page.js');
|
|
12
13
|
var redirectWithExitiframe = require('../helpers/redirect-with-exitiframe.js');
|
|
13
14
|
var beginAuth = require('../helpers/begin-auth.js');
|
|
15
|
+
var ensureCorsHeaders = require('../helpers/ensure-cors-headers.js');
|
|
14
16
|
var validateSessionToken = require('../helpers/validate-session-token.js');
|
|
15
17
|
var getSessionTokenHeader = require('../helpers/get-session-token-header.js');
|
|
16
18
|
var rejectBotRequest = require('../helpers/reject-bot-request.js');
|
|
17
|
-
var
|
|
19
|
+
var respondToOptionsRequest = require('../helpers/respond-to-options-request.js');
|
|
18
20
|
var graphqlClient = require('./graphql-client.js');
|
|
19
21
|
var restClient = require('./rest-client.js');
|
|
20
22
|
|
|
@@ -43,20 +45,30 @@ class AuthStrategy {
|
|
|
43
45
|
logger,
|
|
44
46
|
config
|
|
45
47
|
}, request);
|
|
46
|
-
|
|
48
|
+
respondToOptionsRequest.respondToOptionsRequest({
|
|
49
|
+
api,
|
|
50
|
+
logger,
|
|
51
|
+
config
|
|
52
|
+
}, request);
|
|
53
|
+
const cors = ensureCorsHeaders.ensureCORSHeadersFactory({
|
|
54
|
+
api,
|
|
55
|
+
logger,
|
|
56
|
+
config
|
|
57
|
+
}, request);
|
|
47
58
|
let sessionContext;
|
|
48
59
|
try {
|
|
49
60
|
sessionContext = await this.authenticateAndGetSessionContext(request);
|
|
50
61
|
} catch (errorOrResponse) {
|
|
51
62
|
if (errorOrResponse instanceof Response) {
|
|
52
|
-
|
|
63
|
+
cors(errorOrResponse);
|
|
53
64
|
}
|
|
54
65
|
throw errorOrResponse;
|
|
55
66
|
}
|
|
56
67
|
const context = {
|
|
57
68
|
admin: this.createAdminApiContext(request, sessionContext.session),
|
|
58
69
|
billing: this.createBillingContext(request, sessionContext.session),
|
|
59
|
-
session: sessionContext.session
|
|
70
|
+
session: sessionContext.session,
|
|
71
|
+
cors
|
|
60
72
|
};
|
|
61
73
|
if (config.isEmbeddedApp) {
|
|
62
74
|
return {
|
|
@@ -82,13 +94,13 @@ class AuthStrategy {
|
|
|
82
94
|
logger.info('Authenticating admin request');
|
|
83
95
|
if (isPatchSessionToken) {
|
|
84
96
|
logger.debug('Rendering bounce page');
|
|
85
|
-
throw this.renderAppBridge();
|
|
97
|
+
throw this.renderAppBridge(request);
|
|
86
98
|
} else if (isExitIframe) {
|
|
87
99
|
const destination = url.searchParams.get('exitIframe');
|
|
88
100
|
logger.debug('Rendering exit iframe page', {
|
|
89
101
|
destination
|
|
90
102
|
});
|
|
91
|
-
throw this.renderAppBridge(destination);
|
|
103
|
+
throw this.renderAppBridge(request, destination);
|
|
92
104
|
} else if (isAuthCallbackRequest) {
|
|
93
105
|
throw await this.handleAuthCallbackRequest(request);
|
|
94
106
|
} else if (isAuthRequest) {
|
|
@@ -477,7 +489,7 @@ class AuthStrategy {
|
|
|
477
489
|
// https://github.com/orgs/Shopify/projects/6899/views/1?pane=issue&itemId=28376650
|
|
478
490
|
throw serverRuntime.redirect(`${config.auth.patchSessionTokenPath}?${params.toString()}`);
|
|
479
491
|
}
|
|
480
|
-
renderAppBridge(redirectTo) {
|
|
492
|
+
renderAppBridge(request, redirectTo) {
|
|
481
493
|
const {
|
|
482
494
|
config
|
|
483
495
|
} = this;
|
|
@@ -486,34 +498,17 @@ class AuthStrategy {
|
|
|
486
498
|
const redirectUrl = decodeURIComponent(redirectTo.startsWith('/') ? `${config.appUrl}${redirectTo}` : redirectTo);
|
|
487
499
|
redirectToScript = `<script>window.open("${redirectUrl}", "_top")</script>`;
|
|
488
500
|
}
|
|
501
|
+
const responseHeaders = new Headers({
|
|
502
|
+
'content-type': 'text/html;charset=utf-8'
|
|
503
|
+
});
|
|
504
|
+
addResponseHeaders.addDocumentResponseHeaders(responseHeaders, config.isEmbeddedApp, new URL(request.url).searchParams.get('shop'));
|
|
489
505
|
throw new Response(`
|
|
490
|
-
<script data-api-key="${config.apiKey}" src="${
|
|
506
|
+
<script data-api-key="${config.apiKey}" src="${appBridgeUrl.appBridgeUrl()}"></script>
|
|
491
507
|
${redirectToScript}
|
|
492
508
|
`, {
|
|
493
|
-
headers:
|
|
494
|
-
'content-type': 'text/html;charset=utf-8'
|
|
495
|
-
}
|
|
509
|
+
headers: responseHeaders
|
|
496
510
|
});
|
|
497
511
|
}
|
|
498
|
-
respondToOptionsRequest(request) {
|
|
499
|
-
if (request.method === 'OPTIONS') {
|
|
500
|
-
throw new Response(null, {
|
|
501
|
-
status: 204,
|
|
502
|
-
headers: {
|
|
503
|
-
'Access-Control-Max-Age': '7200'
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
ensureResponseHeaders(request, response) {
|
|
509
|
-
const {
|
|
510
|
-
config
|
|
511
|
-
} = this;
|
|
512
|
-
const shop = new URL(request.url).searchParams.get('shop');
|
|
513
|
-
|
|
514
|
-
// We want the headers to be present on all responses:
|
|
515
|
-
addResponseHeaders.addResponseHeaders(response.headers, config.isEmbeddedApp, shop);
|
|
516
|
-
}
|
|
517
512
|
overriddenRestClient(request, session) {
|
|
518
513
|
const {
|
|
519
514
|
api,
|
package/build/cjs/auth/const.js
CHANGED
|
@@ -4,14 +4,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
const APP_BRIDGE_URL = 'https://cdn.shopify.com/shopifycloud/app-bridge.js';
|
|
6
6
|
const REAUTH_URL_HEADER = 'X-Shopify-API-Request-Failure-Reauthorize-Url';
|
|
7
|
-
const APP_BRIDGE_HEADERS = {
|
|
8
|
-
'Access-Control-Allow-Origin': '*',
|
|
9
|
-
'Access-Control-Allow-Headers': 'Authorization',
|
|
10
|
-
'Access-Control-Expose-Headers': REAUTH_URL_HEADER
|
|
11
|
-
};
|
|
12
|
-
const DEFAULT_CSP_VALUE = `frame-ancestors https://*.myshopify.com https://admin.shopify.com;`;
|
|
13
7
|
|
|
14
|
-
exports.APP_BRIDGE_HEADERS = APP_BRIDGE_HEADERS;
|
|
15
8
|
exports.APP_BRIDGE_URL = APP_BRIDGE_URL;
|
|
16
|
-
exports.DEFAULT_CSP_VALUE = DEFAULT_CSP_VALUE;
|
|
17
9
|
exports.REAUTH_URL_HEADER = REAUTH_URL_HEADER;
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const ORIGINAL_HEADERS = Symbol.for('originalHeaders');
|
|
8
|
-
function addResponseHeadersFactory(params) {
|
|
5
|
+
function addDocumentResponseHeadersFactory(params) {
|
|
9
6
|
const {
|
|
10
7
|
api,
|
|
11
8
|
config
|
|
@@ -15,55 +12,21 @@ function addResponseHeadersFactory(params) {
|
|
|
15
12
|
searchParams
|
|
16
13
|
} = new URL(request.url);
|
|
17
14
|
const shop = api.utils.sanitizeShop(searchParams.get('shop'));
|
|
18
|
-
|
|
15
|
+
addDocumentResponseHeaders(headers, config.isEmbeddedApp, shop);
|
|
19
16
|
};
|
|
20
17
|
}
|
|
21
|
-
function
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
let csp = headers.get('Content-Security-Policy') || _const.DEFAULT_CSP_VALUE;
|
|
25
|
-
if (shop) csp = csp.replace('*.myshopify.com', shop);
|
|
26
|
-
headers.set('Content-Security-Policy', csp);
|
|
18
|
+
function addDocumentResponseHeaders(headers, isEmbeddedApp, shop) {
|
|
19
|
+
if (shop) {
|
|
20
|
+
headers.set('Link', '<https://cdn.shopify.com/shopifycloud/app-bridge.js>; rel="preload"; as="script";');
|
|
27
21
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Store the original Response.prototype.headers descriptor so we can use it when live-reloading the server:
|
|
33
|
-
Object.defineProperty(Response.prototype, ORIGINAL_HEADERS, headersDescriptor);
|
|
34
|
-
|
|
35
|
-
// Overwrite the Response.prototype.headers getter with one that injects our global headers:
|
|
36
|
-
Object.defineProperty(Response.prototype, 'headers', {
|
|
37
|
-
...headersDescriptor,
|
|
38
|
-
get() {
|
|
39
|
-
// Note: this is not an empty Headers, it will contain any headers passed to new Response():
|
|
40
|
-
const headers = headersDescriptor.get.call(this);
|
|
41
|
-
try {
|
|
42
|
-
// Inject CORS headers required by both Embedded Apps and UI Extensions:
|
|
43
|
-
if (isEmbeddedApp) {
|
|
44
|
-
for (const key in _const.APP_BRIDGE_HEADERS) {
|
|
45
|
-
if (!headers.get(key)) {
|
|
46
|
-
const value = _const.APP_BRIDGE_HEADERS[key];
|
|
47
|
-
headers.set(key, value);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Apply a default CSP unless a more specific one (no wildcard) is present:
|
|
53
|
-
if (!headers.get('Content-Security-Policy')) {
|
|
54
|
-
headers.set('Content-Security-Policy', isEmbeddedApp ? _const.DEFAULT_CSP_VALUE : `frame-ancestors 'none';`);
|
|
55
|
-
}
|
|
56
|
-
if (!headers.get('Link')) {
|
|
57
|
-
headers.set('Link', `<${_const.APP_BRIDGE_URL}>; rel="preload"`);
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
// Do nothing, this is not a standard Response object.
|
|
61
|
-
}
|
|
62
|
-
return headers;
|
|
22
|
+
if (isEmbeddedApp) {
|
|
23
|
+
if (shop) {
|
|
24
|
+
headers.set('Content-Security-Policy', `frame-ancestors https://${shop} https://admin.shopify.com;`);
|
|
63
25
|
}
|
|
64
|
-
}
|
|
26
|
+
} else {
|
|
27
|
+
headers.set('Content-Security-Policy', `frame-ancestors 'none';`);
|
|
28
|
+
}
|
|
65
29
|
}
|
|
66
30
|
|
|
67
|
-
exports.
|
|
68
|
-
exports.
|
|
69
|
-
exports.installGlobalResponseHeaders = installGlobalResponseHeaders;
|
|
31
|
+
exports.addDocumentResponseHeaders = addDocumentResponseHeaders;
|
|
32
|
+
exports.addDocumentResponseHeadersFactory = addDocumentResponseHeadersFactory;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var _const = require('../const.js');
|
|
6
|
+
|
|
7
|
+
let appBridgeUrlOverride;
|
|
8
|
+
function setAppBridgeUrlOverride(url) {
|
|
9
|
+
appBridgeUrlOverride = url;
|
|
10
|
+
}
|
|
11
|
+
function appBridgeUrl() {
|
|
12
|
+
return appBridgeUrlOverride || _const.APP_BRIDGE_URL;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.appBridgeUrl = appBridgeUrl;
|
|
16
|
+
exports.setAppBridgeUrlOverride = setAppBridgeUrlOverride;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var _const = require('../const.js');
|
|
6
|
+
|
|
7
|
+
function ensureCORSHeadersFactory(params, request) {
|
|
8
|
+
const {
|
|
9
|
+
logger,
|
|
10
|
+
config
|
|
11
|
+
} = params;
|
|
12
|
+
return function ensureCORSHeaders(response) {
|
|
13
|
+
const origin = request.headers.get('Origin');
|
|
14
|
+
if (origin && origin !== config.appUrl) {
|
|
15
|
+
logger.debug('Request comes from a different origin, adding CORS headers');
|
|
16
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
17
|
+
response.headers.set('Access-Control-Allow-Headers', 'Authorization');
|
|
18
|
+
response.headers.set('Access-Control-Expose-Headers', _const.REAUTH_URL_HEADER);
|
|
19
|
+
}
|
|
20
|
+
return response;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.ensureCORSHeadersFactory = ensureCORSHeadersFactory;
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var _const = require('../const.js');
|
|
6
|
-
var addResponseHeaders = require('./add-response-headers.js');
|
|
7
6
|
|
|
8
7
|
function redirectWithAppBridgeHeaders(params, shop) {
|
|
9
8
|
const {
|
|
@@ -13,18 +12,13 @@ function redirectWithAppBridgeHeaders(params, shop) {
|
|
|
13
12
|
throw new Response(undefined, {
|
|
14
13
|
status: 401,
|
|
15
14
|
statusText: 'Unauthorized',
|
|
16
|
-
headers: getAppBridgeHeaders(
|
|
15
|
+
headers: getAppBridgeHeaders(redirectUri)
|
|
17
16
|
});
|
|
18
17
|
}
|
|
19
|
-
function getAppBridgeHeaders(
|
|
20
|
-
|
|
21
|
-
config
|
|
22
|
-
} = params;
|
|
23
|
-
const headers = new Headers({
|
|
18
|
+
function getAppBridgeHeaders(url) {
|
|
19
|
+
return new Headers({
|
|
24
20
|
[_const.REAUTH_URL_HEADER]: url
|
|
25
21
|
});
|
|
26
|
-
addResponseHeaders.addResponseHeaders(headers, config.isEmbeddedApp, shop);
|
|
27
|
-
return headers;
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
exports.getAppBridgeHeaders = getAppBridgeHeaders;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var ensureCorsHeaders = require('./ensure-cors-headers.js');
|
|
6
|
+
|
|
7
|
+
function respondToOptionsRequest(params, request) {
|
|
8
|
+
if (request.method === 'OPTIONS') {
|
|
9
|
+
const ensureCORSHeaders = ensureCorsHeaders.ensureCORSHeadersFactory(params, request);
|
|
10
|
+
throw ensureCORSHeaders(new Response(null, {
|
|
11
|
+
status: 204,
|
|
12
|
+
headers: {
|
|
13
|
+
'Access-Control-Max-Age': '7200'
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.respondToOptionsRequest = respondToOptionsRequest;
|
|
@@ -12,12 +12,6 @@ function loginFactory(params) {
|
|
|
12
12
|
logger
|
|
13
13
|
} = params;
|
|
14
14
|
return async function login(request) {
|
|
15
|
-
if (!config.canUseLoginForm) {
|
|
16
|
-
logger.debug('Login endpoint not available for Shopify Admin custom apps');
|
|
17
|
-
throw new Response(undefined, {
|
|
18
|
-
status: 404
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
15
|
const url = new URL(request.url);
|
|
22
16
|
const shopParam = url.searchParams.get('shop');
|
|
23
17
|
if (request.method === 'GET' && !shopParam) {
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
require('@remix-run/server-runtime');
|
|
6
|
+
var ensureCorsHeaders = require('../helpers/ensure-cors-headers.js');
|
|
6
7
|
var validateSessionToken = require('../helpers/validate-session-token.js');
|
|
7
8
|
var getSessionTokenHeader = require('../helpers/get-session-token-header.js');
|
|
8
9
|
var rejectBotRequest = require('../helpers/reject-bot-request.js');
|
|
10
|
+
var respondToOptionsRequest = require('../helpers/respond-to-options-request.js');
|
|
9
11
|
require('@shopify/shopify-api');
|
|
10
12
|
|
|
11
13
|
function authenticatePublicFactory(params) {
|
|
@@ -14,6 +16,7 @@ function authenticatePublicFactory(params) {
|
|
|
14
16
|
logger
|
|
15
17
|
} = params;
|
|
16
18
|
rejectBotRequest.rejectBotRequest(params, request);
|
|
19
|
+
respondToOptionsRequest.respondToOptionsRequest(params, request);
|
|
17
20
|
const sessionTokenHeader = getSessionTokenHeader.getSessionTokenHeader(request);
|
|
18
21
|
logger.info('Authenticating public request');
|
|
19
22
|
if (!sessionTokenHeader) {
|
|
@@ -24,7 +27,8 @@ function authenticatePublicFactory(params) {
|
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
return {
|
|
27
|
-
sessionToken: await validateSessionToken.validateSessionToken(params, sessionTokenHeader)
|
|
30
|
+
sessionToken: await validateSessionToken.validateSessionToken(params, sessionTokenHeader),
|
|
31
|
+
cors: ensureCorsHeaders.ensureCORSHeadersFactory(params, request)
|
|
28
32
|
};
|
|
29
33
|
};
|
|
30
34
|
}
|
|
@@ -65,7 +65,7 @@ function redirectOutOfApp(params, request, url, shop) {
|
|
|
65
65
|
throw new Response(undefined, {
|
|
66
66
|
status: 401,
|
|
67
67
|
statusText: 'Unauthorized',
|
|
68
|
-
headers: redirectWithAppBridgeHeaders.getAppBridgeHeaders(
|
|
68
|
+
headers: redirectWithAppBridgeHeaders.getAppBridgeHeaders(url)
|
|
69
69
|
});
|
|
70
70
|
} else if (isEmbeddedRequest) {
|
|
71
71
|
const params = new URLSearchParams({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
function errorBoundary(error) {
|
|
6
|
+
if (error.constructor.name === 'ErrorResponse') {
|
|
7
|
+
return 'Handling response';
|
|
8
|
+
}
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exports.errorBoundary = errorBoundary;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
function headersBoundary(headers) {
|
|
6
|
+
const {
|
|
7
|
+
parentHeaders,
|
|
8
|
+
loaderHeaders,
|
|
9
|
+
actionHeaders,
|
|
10
|
+
errorHeaders
|
|
11
|
+
} = headers;
|
|
12
|
+
if (errorHeaders && Array.from(errorHeaders.entries()).length > 0) {
|
|
13
|
+
return errorHeaders;
|
|
14
|
+
}
|
|
15
|
+
return new Headers([...(parentHeaders ? Array.from(parentHeaders.entries()) : []), ...(loaderHeaders ? Array.from(loaderHeaders.entries()) : []), ...(actionHeaders ? Array.from(actionHeaders.entries()) : [])]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
exports.headersBoundary = headersBoundary;
|