@shopify/shopify-app-remix 1.0.0-rc.0 → 1.0.0-rc.2
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 +74 -26
- package/build/cjs/adapters/node/index.js +10 -0
- package/build/cjs/auth/admin/authenticate.js +25 -30
- package/build/cjs/auth/const.js +2 -10
- 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 +2 -3
- package/build/ts/auth/const.d.ts.map +1 -1
- package/build/ts/auth/const.js +3 -4
- 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.js +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,13 +16,6 @@ To follow these usage guides, you will need to:
|
|
|
16
16
|
- have an app already set up on your partner account
|
|
17
17
|
- have a JavaScript package manager such as [yarn](https://yarnpkg.com) installed
|
|
18
18
|
|
|
19
|
-
## Shopify CLI
|
|
20
|
-
|
|
21
|
-
The easiest way to get started with developing Shopify apps is using the [Shopify CLI](https://shopify.dev/docs/apps/tools/cli).
|
|
22
|
-
It helps you set up your environment for developing and publishing your apps and extensions.
|
|
23
|
-
|
|
24
|
-
We strongly recommend using the CLI to create and manage your apps and extensions!
|
|
25
|
-
|
|
26
19
|
## Getting started
|
|
27
20
|
|
|
28
21
|
This package works with any Remix app. If you're starting an app from scratch, you can create a brand new Remix app that uses the indie-stack:
|
|
@@ -45,13 +38,14 @@ Create `app/shopify.server.js`. We will use this file to configure our Shopify a
|
|
|
45
38
|
import '@shopify/shopify-app-remix/adapters/node';
|
|
46
39
|
import {LATEST_API_VERSION, shopifyApp} from '@shopify/shopify-app-remix';
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
const shopify = shopifyApp({
|
|
49
42
|
apiKey: process.env.SHOPIFY_API_KEY!,
|
|
50
43
|
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
|
|
51
44
|
appUrl: process.env.SHOPIFY_APP_URL!,
|
|
52
45
|
scopes: ['read_products'],
|
|
53
46
|
apiVersion: LATEST_API_VERSION,
|
|
54
47
|
});
|
|
48
|
+
export default shopify;
|
|
55
49
|
```
|
|
56
50
|
|
|
57
51
|
A description of these config options:
|
|
@@ -79,7 +73,7 @@ SHOPIFY_APP_URL="[The tunnel URL you are using to run your app]"
|
|
|
79
73
|
// app/shopify.server.js
|
|
80
74
|
import {shopifyApp} from '@shopify/shopify-app-remix';
|
|
81
75
|
|
|
82
|
-
|
|
76
|
+
const shopify = shopifyApp({
|
|
83
77
|
// ...
|
|
84
78
|
authPathPrefix: '/auth',
|
|
85
79
|
});
|
|
@@ -91,7 +85,7 @@ Now let's create the [splat route](https://remix.run/docs/en/main/guides/routing
|
|
|
91
85
|
// app/routes/auth/$.tsx
|
|
92
86
|
import {LoaderArgs} from '@remix-run/node';
|
|
93
87
|
|
|
94
|
-
import
|
|
88
|
+
import shopify from '../../shopify.server';
|
|
95
89
|
|
|
96
90
|
export async function loader({request}: LoaderArgs) {
|
|
97
91
|
await shopify.authenticate.admin(request);
|
|
@@ -122,7 +116,7 @@ export default function App() {
|
|
|
122
116
|
<Links />
|
|
123
117
|
{/* App Bridge must be loaded from the CDN at the head */}
|
|
124
118
|
<script
|
|
125
|
-
src="https://cdn.shopify.com/shopifycloud/app-bridge
|
|
119
|
+
src="https://cdn.shopify.com/shopifycloud/app-bridge.js"
|
|
126
120
|
data-api-key={apiKey}
|
|
127
121
|
/>
|
|
128
122
|
</head>
|
|
@@ -135,14 +129,14 @@ export default function App() {
|
|
|
135
129
|
> **Note**: This version of App Bridge must be loaded from the CDN, in the document head.
|
|
136
130
|
|
|
137
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).
|
|
138
|
-
To do that, this package provides the `shopify.
|
|
132
|
+
To do that, this package provides the `shopify.addDocumentResponseHeaders` method.
|
|
139
133
|
|
|
140
134
|
You should return these headers from any endpoint that renders HTML in your app.
|
|
141
135
|
Most likely you'll want to add this to every HTML response by updating the entry.server.tsx file:
|
|
142
136
|
|
|
143
137
|
```ts
|
|
144
138
|
// entry.server.tsx
|
|
145
|
-
import
|
|
139
|
+
import shopify from './shopify.server';
|
|
146
140
|
|
|
147
141
|
export default function handleRequest(
|
|
148
142
|
request: Request,
|
|
@@ -155,7 +149,7 @@ export default function handleRequest(
|
|
|
155
149
|
);
|
|
156
150
|
|
|
157
151
|
responseHeaders.set('Content-Type', 'text/html');
|
|
158
|
-
shopify.
|
|
152
|
+
shopify.addDocumentResponseHeaders(request, responseHeaders);
|
|
159
153
|
|
|
160
154
|
return new Response('<!DOCTYPE html>' + markup, {
|
|
161
155
|
status: responseStatusCode,
|
|
@@ -191,7 +185,8 @@ To load your app within the Shopify Admin, you need to:
|
|
|
191
185
|
|
|
192
186
|
## Authenticating admin requests
|
|
193
187
|
|
|
194
|
-
`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:
|
|
195
190
|
|
|
196
191
|
```ts
|
|
197
192
|
// app/routes/**/*.tsx
|
|
@@ -202,7 +197,44 @@ export const loader = async ({request}: LoaderArgs) => {
|
|
|
202
197
|
};
|
|
203
198
|
```
|
|
204
199
|
|
|
205
|
-
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.
|
|
206
238
|
|
|
207
239
|
### Using the Shopify admin GraphQL API
|
|
208
240
|
|
|
@@ -212,7 +244,7 @@ E.g:
|
|
|
212
244
|
|
|
213
245
|
```ts
|
|
214
246
|
// routes/**/*.tsx
|
|
215
|
-
import
|
|
247
|
+
import shopify from '../shopify.server';
|
|
216
248
|
import {ActionArgs, json} from '@remix-run/node';
|
|
217
249
|
|
|
218
250
|
export async function action({request}: ActionArgs) {
|
|
@@ -250,7 +282,7 @@ export async function action({request}: ActionArgs) {
|
|
|
250
282
|
// app/routes/**/*.tsx
|
|
251
283
|
import {restResources} from '@shopify/shopify-api/rest/admin/2023-04';
|
|
252
284
|
|
|
253
|
-
|
|
285
|
+
const shopify = shopifyApp({
|
|
254
286
|
restResources,
|
|
255
287
|
// ...etc
|
|
256
288
|
});
|
|
@@ -281,7 +313,7 @@ To setup webhooks first we need to configure `shopifyApp` with 2 pieces:
|
|
|
281
313
|
// shopify.server.js
|
|
282
314
|
import {shopifyApp, DeliveryMethod} from '@shopify/shopify-app-remix';
|
|
283
315
|
|
|
284
|
-
|
|
316
|
+
const shopify = shopifyApp({
|
|
285
317
|
apiKey: '1707326264fde5037c658n120626ce3f',
|
|
286
318
|
// ...etc
|
|
287
319
|
webhooks: {
|
|
@@ -306,7 +338,7 @@ To do this, your app must authenticate the request.
|
|
|
306
338
|
// routes/webhooks.tsx
|
|
307
339
|
import {ActionArgs} from '@remix-run/node';
|
|
308
340
|
|
|
309
|
-
import
|
|
341
|
+
import shopify from '../shopify.server';
|
|
310
342
|
import db from '../db.server';
|
|
311
343
|
|
|
312
344
|
export const action = async ({request}: ActionArgs) => {
|
|
@@ -332,24 +364,28 @@ export const action = async ({request}: ActionArgs) => {
|
|
|
332
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:
|
|
333
365
|
|
|
334
366
|
```ts
|
|
335
|
-
// e.g: routes/api
|
|
336
|
-
import
|
|
367
|
+
// e.g: routes/api.public.notes.tsx
|
|
368
|
+
import shopify from '../shopify.server';
|
|
337
369
|
import {LoaderArgs, json} from '@remix-run/node';
|
|
338
370
|
import {getNotes} from '~/models/notes';
|
|
339
371
|
|
|
340
372
|
export const loader = async ({request}: LoaderArgs) => {
|
|
341
|
-
const {sessionToken} = await shopify.authenticate.public(request);
|
|
373
|
+
const {sessionToken, cors} = await shopify.authenticate.public(request);
|
|
342
374
|
|
|
343
375
|
// E.g: Get notes using the shops admin domain
|
|
344
|
-
return json(await getNotes(sessionToken.iss));
|
|
376
|
+
return cors(json(await getNotes(sessionToken.iss)));
|
|
345
377
|
};
|
|
346
378
|
```
|
|
347
379
|
|
|
348
380
|
This can be useful if your app exposes checkout or theme extensions and those extensions need to access data from your app.
|
|
349
381
|
|
|
382
|
+
**Note:** These requests will be cross-origin. So you must use the cross-origin helper returned from `shopify.authenticate.public`.
|
|
383
|
+
|
|
350
384
|
## Session Storage
|
|
351
385
|
|
|
352
|
-
|
|
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).
|
|
353
389
|
|
|
354
390
|
In this example we'll swap the default session adaptor for [Prisma](https://www.prisma.io/).
|
|
355
391
|
|
|
@@ -370,4 +406,16 @@ const shopify = shopifyApp({
|
|
|
370
406
|
});
|
|
371
407
|
```
|
|
372
408
|
|
|
373
|
-
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
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
const
|
|
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.
|
|
15
|
-
exports.APP_BRIDGE_NEXT_URL = APP_BRIDGE_NEXT_URL;
|
|
16
|
-
exports.DEFAULT_CSP_VALUE = DEFAULT_CSP_VALUE;
|
|
8
|
+
exports.APP_BRIDGE_URL = APP_BRIDGE_URL;
|
|
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_NEXT_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;
|