@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.
Files changed (90) hide show
  1. package/README.md +74 -26
  2. package/build/cjs/adapters/node/index.js +10 -0
  3. package/build/cjs/auth/admin/authenticate.js +25 -30
  4. package/build/cjs/auth/const.js +2 -10
  5. package/build/cjs/auth/helpers/add-response-headers.js +13 -50
  6. package/build/cjs/auth/helpers/app-bridge-url.js +16 -0
  7. package/build/cjs/auth/helpers/ensure-cors-headers.js +24 -0
  8. package/build/cjs/auth/helpers/redirect-with-app-bridge-headers.js +3 -9
  9. package/build/cjs/auth/helpers/respond-to-options-request.js +19 -0
  10. package/build/cjs/auth/login/login.js +0 -6
  11. package/build/cjs/auth/public/authenticate.js +5 -1
  12. package/build/cjs/billing/request.js +1 -1
  13. package/build/cjs/boundary/error.js +12 -0
  14. package/build/cjs/boundary/headers.js +18 -0
  15. package/build/cjs/index.js +57 -8
  16. package/build/cjs/types.js +1 -0
  17. package/build/cjs/version.js +1 -1
  18. package/build/ts/adapters/node/index.js +6 -0
  19. package/build/ts/adapters/node/index.js.map +1 -1
  20. package/build/ts/auth/admin/authenticate.d.ts +0 -2
  21. package/build/ts/auth/admin/authenticate.d.ts.map +1 -1
  22. package/build/ts/auth/admin/authenticate.js +13 -25
  23. package/build/ts/auth/admin/authenticate.js.map +1 -1
  24. package/build/ts/auth/admin/types.d.ts +32 -13
  25. package/build/ts/auth/admin/types.d.ts.map +1 -1
  26. package/build/ts/auth/const.d.ts +2 -3
  27. package/build/ts/auth/const.d.ts.map +1 -1
  28. package/build/ts/auth/const.js +3 -4
  29. package/build/ts/auth/const.js.map +1 -1
  30. package/build/ts/auth/helpers/add-response-headers.d.ts +2 -3
  31. package/build/ts/auth/helpers/add-response-headers.d.ts.map +1 -1
  32. package/build/ts/auth/helpers/add-response-headers.js +16 -49
  33. package/build/ts/auth/helpers/add-response-headers.js.map +1 -1
  34. package/build/ts/auth/helpers/app-bridge-url.d.ts +3 -0
  35. package/build/ts/auth/helpers/app-bridge-url.d.ts.map +1 -0
  36. package/build/ts/auth/helpers/app-bridge-url.js +14 -0
  37. package/build/ts/auth/helpers/app-bridge-url.js.map +1 -0
  38. package/build/ts/auth/helpers/ensure-cors-headers.d.ts +6 -0
  39. package/build/ts/auth/helpers/ensure-cors-headers.d.ts.map +1 -0
  40. package/build/ts/auth/helpers/ensure-cors-headers.js +19 -0
  41. package/build/ts/auth/helpers/ensure-cors-headers.js.map +1 -0
  42. package/build/ts/auth/helpers/index.d.ts +11 -7
  43. package/build/ts/auth/helpers/index.d.ts.map +1 -1
  44. package/build/ts/auth/helpers/index.js +12 -15
  45. package/build/ts/auth/helpers/index.js.map +1 -1
  46. package/build/ts/auth/helpers/redirect-with-app-bridge-headers.d.ts +1 -1
  47. package/build/ts/auth/helpers/redirect-with-app-bridge-headers.d.ts.map +1 -1
  48. package/build/ts/auth/helpers/redirect-with-app-bridge-headers.js +3 -7
  49. package/build/ts/auth/helpers/redirect-with-app-bridge-headers.js.map +1 -1
  50. package/build/ts/auth/helpers/respond-to-options-request.d.ts +3 -0
  51. package/build/ts/auth/helpers/respond-to-options-request.d.ts.map +1 -0
  52. package/build/ts/auth/helpers/respond-to-options-request.js +17 -0
  53. package/build/ts/auth/helpers/respond-to-options-request.js.map +1 -0
  54. package/build/ts/auth/login/login.d.ts.map +1 -1
  55. package/build/ts/auth/login/login.js +0 -4
  56. package/build/ts/auth/login/login.js.map +1 -1
  57. package/build/ts/auth/public/authenticate.d.ts.map +1 -1
  58. package/build/ts/auth/public/authenticate.js +2 -0
  59. package/build/ts/auth/public/authenticate.js.map +1 -1
  60. package/build/ts/auth/public/types.d.ts +25 -2
  61. package/build/ts/auth/public/types.d.ts.map +1 -1
  62. package/build/ts/billing/request.js +1 -1
  63. package/build/ts/billing/request.js.map +1 -1
  64. package/build/ts/billing/types.d.ts +20 -12
  65. package/build/ts/billing/types.d.ts.map +1 -1
  66. package/build/ts/boundary/error.d.ts +2 -0
  67. package/build/ts/boundary/error.d.ts.map +1 -0
  68. package/build/ts/boundary/error.js +11 -0
  69. package/build/ts/boundary/error.js.map +1 -0
  70. package/build/ts/boundary/headers.d.ts +3 -0
  71. package/build/ts/boundary/headers.d.ts.map +1 -0
  72. package/build/ts/boundary/headers.js +13 -0
  73. package/build/ts/boundary/headers.js.map +1 -0
  74. package/build/ts/boundary/types.d.ts +4 -0
  75. package/build/ts/boundary/types.d.ts.map +1 -0
  76. package/build/ts/boundary/types.js +3 -0
  77. package/build/ts/boundary/types.js.map +1 -0
  78. package/build/ts/config-types.d.ts +28 -18
  79. package/build/ts/config-types.d.ts.map +1 -1
  80. package/build/ts/index.d.ts +39 -1
  81. package/build/ts/index.d.ts.map +1 -1
  82. package/build/ts/index.js +56 -10
  83. package/build/ts/index.js.map +1 -1
  84. package/build/ts/types.d.ts +88 -122
  85. package/build/ts/types.d.ts.map +1 -1
  86. package/build/ts/types.js.map +1 -1
  87. package/build/ts/version.d.ts +1 -1
  88. package/build/ts/version.js +1 -1
  89. package/build/tsconfig.tsbuildinfo +1 -1
  90. 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
- export const shopify = shopifyApp({
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
- export const shopify = shopifyApp({
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 {shopify} from '../../shopify.server';
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-next.js"
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.addResponseHeaders` method.
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 {shopify} from '~/shopify.server';
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.addResponseHeaders(request, responseHeaders);
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. To authenticate admin requests you can call `shopify.authenticate.admin(request)` in a loader or an action:
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. If there is no session for the user, the loader will throw the appropriate redirect Response.
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 {shopify} from '../shopify.server';
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
- export const shopify = shopifyApp({
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
- export const shopify = shopifyApp({
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 {shopify} from '../shopify.server';
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/public/notes.tsx
336
- import {shopify} from '../shopify.server';
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
- By default `shopifyApp` uses [@shopify/shopify-app-session-storage-sqlite](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-session-storage-sqlite) to store sessions. You can change this by passing a different Session Adaptor to `shopifyApp`. To make this easy Shopify offer's [7 production ready session adaptors](https://github.com/Shopify/shopify-app-js/tree/release-candidate/packages)
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 _const = require('../const.js');
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
- this.respondToOptionsRequest(request);
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
- this.ensureResponseHeaders(request, errorOrResponse);
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="${_const.APP_BRIDGE_NEXT_URL}"></script>
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,
@@ -2,16 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- const APP_BRIDGE_NEXT_URL = 'https://cdn.shopify.com/shopifycloud/app-bridge-next.js';
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
- 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
- var _const = require('../const.js');
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
- addResponseHeaders(headers, config.isEmbeddedApp, shop);
15
+ addDocumentResponseHeaders(headers, config.isEmbeddedApp, shop);
19
16
  };
20
17
  }
21
- function addResponseHeaders(headers, isEmbeddedApp, shop) {
22
- if (isEmbeddedApp && shop) {
23
- // Set or update the CSP with the shop subdomain instead of a wildcard:
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
- function installGlobalResponseHeaders(isEmbeddedApp) {
30
- const headersDescriptor = Object.getOwnPropertyDescriptor(Response.prototype, ORIGINAL_HEADERS) || Object.getOwnPropertyDescriptor(Response.prototype, 'headers');
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.addResponseHeaders = addResponseHeaders;
68
- exports.addResponseHeadersFactory = addResponseHeadersFactory;
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(params, redirectUri, shop)
15
+ headers: getAppBridgeHeaders(redirectUri)
17
16
  });
18
17
  }
19
- function getAppBridgeHeaders(params, url, shop) {
20
- const {
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(params, url, shop)
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;