@shopify/shopify-app-remix 1.0.0-rc.1 → 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 +73 -18
  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 +0 -8
  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 +1 -2
  27. package/build/ts/auth/const.d.ts.map +1 -1
  28. package/build/ts/auth/const.js +2 -3
  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 +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
- export const shopify = shopifyApp({
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
- export const shopify = shopifyApp({
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 {shopify} from '../../shopify.server';
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.addResponseHeaders` method.
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 {shopify} from '~/shopify.server';
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.addResponseHeaders(request, responseHeaders);
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. 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:
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. 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.
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 {shopify} from '../shopify.server';
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
- export const shopify = shopifyApp({
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
- export const shopify = shopifyApp({
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 {shopify} from '../shopify.server';
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/public/notes.tsx
329
- import {shopify} from '../shopify.server';
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
- 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).
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 _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_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,
@@ -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
- 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_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;