@shopify/shop-minis-react 0.4.15 → 0.4.16

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.
@@ -31,8 +31,8 @@ const hookPermissionsMap = {
31
31
  useImagePicker: ['CAMERA'],
32
32
  }
33
33
 
34
- // Extract domain from URL (returns domain in CSP format)
35
- function extractDomain(url) {
34
+ // Extract domain and path from URL (returns {hostname, pathname} for CSP matching)
35
+ function extractDomainAndPath(url) {
36
36
  if (!url || typeof url !== 'string') return null
37
37
 
38
38
  try {
@@ -48,25 +48,65 @@ function extractDomain(url) {
48
48
 
49
49
  // Parse URL
50
50
  const urlObj = new URL(url)
51
- return urlObj.hostname
51
+ return {
52
+ hostname: urlObj.hostname,
53
+ pathname: urlObj.pathname,
54
+ }
52
55
  } catch (err) {
53
56
  // If URL parsing fails, might be a relative path
54
57
  return null
55
58
  }
56
59
  }
57
60
 
58
- // Check if domain matches a pattern (supports wildcards)
59
- function domainMatchesPattern(domain, pattern) {
61
+ // Check if a URL matches a trusted_domain pattern
62
+ // Supports: wildcards (*.example.com), paths with CSP semantics
63
+ // CSP path matching: path with trailing slash matches directory, without matches exact or prefix
64
+ function urlMatchesPattern(urlInfo, pattern) {
60
65
  if (pattern === '*') return true
61
- if (pattern === domain) return true
62
66
 
63
- // Handle wildcard subdomains (*.example.com)
64
- if (pattern.startsWith('*.')) {
65
- const basePattern = pattern.slice(2)
66
- return domain === basePattern || domain.endsWith(`.${basePattern}`)
67
+ // Parse the pattern to extract hostname and optional path
68
+ let patternHostname = pattern
69
+ let patternPath = null
70
+
71
+ // Check if pattern contains a path (has / after the domain)
72
+ const slashIndex = pattern.indexOf('/')
73
+ if (slashIndex !== -1) {
74
+ patternHostname = pattern.slice(0, slashIndex)
75
+ patternPath = pattern.slice(slashIndex)
76
+ }
77
+
78
+ // First check hostname match
79
+ let hostnameMatches = false
80
+
81
+ if (patternHostname === urlInfo.hostname) {
82
+ hostnameMatches = true
83
+ } else if (patternHostname.startsWith('*.')) {
84
+ // Handle wildcard subdomains (*.example.com)
85
+ const basePattern = patternHostname.slice(2)
86
+ hostnameMatches =
87
+ urlInfo.hostname === basePattern ||
88
+ urlInfo.hostname.endsWith(`.${basePattern}`)
67
89
  }
68
90
 
69
- return false
91
+ if (!hostnameMatches) {
92
+ return false
93
+ }
94
+
95
+ // If no path in pattern, hostname match is sufficient
96
+ if (!patternPath) {
97
+ return true
98
+ }
99
+
100
+ // CSP path matching semantics:
101
+ // - Path ending with / matches that directory and everything under it (prefix match)
102
+ // - Path without trailing / matches exact path only
103
+ if (patternPath.endsWith('/')) {
104
+ // Directory match: URL path must start with pattern path
105
+ return urlInfo.pathname.startsWith(patternPath)
106
+ } else {
107
+ // Exact match only
108
+ return urlInfo.pathname === patternPath
109
+ }
70
110
  }
71
111
 
72
112
  module.exports = {
@@ -308,10 +348,11 @@ module.exports = {
308
348
  // Check for fetch() calls
309
349
  if (node.callee.name === 'fetch' && firstArg.type === 'Literal') {
310
350
  const url = firstArg.value
311
- const domain = extractDomain(url)
312
- if (domain) {
351
+ const urlInfo = extractDomainAndPath(url)
352
+ if (urlInfo) {
313
353
  requiredDomains.add({
314
- domain,
354
+ urlInfo,
355
+ url,
315
356
  reason: 'fetch() call',
316
357
  node,
317
358
  })
@@ -326,10 +367,11 @@ module.exports = {
326
367
  node.arguments[1].type === 'Literal'
327
368
  ) {
328
369
  const url = node.arguments[1].value
329
- const domain = extractDomain(url)
330
- if (domain) {
370
+ const urlInfo = extractDomainAndPath(url)
371
+ if (urlInfo) {
331
372
  requiredDomains.add({
332
- domain,
373
+ urlInfo,
374
+ url,
333
375
  reason: 'XMLHttpRequest.open() call',
334
376
  node,
335
377
  })
@@ -340,11 +382,12 @@ module.exports = {
340
382
  if (node.callee.name === 'WebSocket' && firstArg.type === 'Literal') {
341
383
  const url = firstArg.value
342
384
  // WebSocket URLs use ws:// or wss://
343
- const domain = url.replace(/^wss?:\/\//, 'https://')
344
- const extracted = extractDomain(domain)
345
- if (extracted) {
385
+ const normalizedUrl = url.replace(/^wss?:\/\//, 'https://')
386
+ const urlInfo = extractDomainAndPath(normalizedUrl)
387
+ if (urlInfo) {
346
388
  requiredDomains.add({
347
- domain: extracted,
389
+ urlInfo,
390
+ url,
348
391
  reason: 'WebSocket connection',
349
392
  node,
350
393
  })
@@ -354,10 +397,11 @@ module.exports = {
354
397
  // Check for EventSource
355
398
  if (node.callee.name === 'EventSource' && firstArg.type === 'Literal') {
356
399
  const url = firstArg.value
357
- const domain = extractDomain(url)
358
- if (domain) {
400
+ const urlInfo = extractDomainAndPath(url)
401
+ if (urlInfo) {
359
402
  requiredDomains.add({
360
- domain,
403
+ urlInfo,
404
+ url,
361
405
  reason: 'EventSource connection',
362
406
  node,
363
407
  })
@@ -371,10 +415,11 @@ module.exports = {
371
415
  firstArg.type === 'Literal'
372
416
  ) {
373
417
  const url = firstArg.value
374
- const domain = extractDomain(url)
375
- if (domain) {
418
+ const urlInfo = extractDomainAndPath(url)
419
+ if (urlInfo) {
376
420
  requiredDomains.add({
377
- domain,
421
+ urlInfo,
422
+ url,
378
423
  reason: 'navigator.sendBeacon() call',
379
424
  node,
380
425
  })
@@ -389,10 +434,11 @@ module.exports = {
389
434
  firstArg.type === 'Literal'
390
435
  ) {
391
436
  const url = firstArg.value
392
- const domain = extractDomain(url)
393
- if (domain) {
437
+ const urlInfo = extractDomainAndPath(url)
438
+ if (urlInfo) {
394
439
  requiredDomains.add({
395
- domain,
440
+ urlInfo,
441
+ url,
396
442
  reason: 'window.open() call',
397
443
  node,
398
444
  })
@@ -499,9 +545,9 @@ module.exports = {
499
545
 
500
546
  const attrName = node.name.name
501
547
  const url = node.value.value
502
- const domain = extractDomain(url)
548
+ const urlInfo = extractDomainAndPath(url)
503
549
 
504
- if (!domain) {
550
+ if (!urlInfo) {
505
551
  return
506
552
  }
507
553
 
@@ -522,7 +568,8 @@ module.exports = {
522
568
  ]
523
569
  if (supportedElements.includes(elementName)) {
524
570
  requiredDomains.add({
525
- domain,
571
+ urlInfo,
572
+ url,
526
573
  reason: `<${elementName}> src attribute`,
527
574
  node,
528
575
  })
@@ -531,7 +578,8 @@ module.exports = {
531
578
  // poster attribute (video)
532
579
  else if (attrName === 'poster') {
533
580
  requiredDomains.add({
534
- domain,
581
+ urlInfo,
582
+ url,
535
583
  reason: `<${elementName}> poster attribute`,
536
584
  node,
537
585
  })
@@ -539,7 +587,8 @@ module.exports = {
539
587
  // data attribute (object)
540
588
  else if (attrName === 'data') {
541
589
  requiredDomains.add({
542
- domain,
590
+ urlInfo,
591
+ url,
543
592
  reason: `<${elementName}> data attribute`,
544
593
  node,
545
594
  })
@@ -547,7 +596,8 @@ module.exports = {
547
596
  // action attribute (form)
548
597
  else if (attrName === 'action') {
549
598
  requiredDomains.add({
550
- domain,
599
+ urlInfo,
600
+ url,
551
601
  reason: `<${elementName}> action attribute`,
552
602
  node,
553
603
  })
@@ -692,26 +742,26 @@ module.exports = {
692
742
  })
693
743
 
694
744
  // Check trusted domains
695
- requiredDomains.forEach(({domain, reason, node}) => {
745
+ requiredDomains.forEach(({urlInfo, url, reason, node}) => {
696
746
  if (!manifest) {
697
747
  issues.push({
698
748
  messageId: 'manifestNotFound',
699
- data: {reason, domain},
749
+ data: {reason, domain: urlInfo.hostname},
700
750
  })
701
751
  return
702
752
  }
703
753
 
704
754
  const trustedDomains = manifest.trusted_domains || []
705
755
 
706
- // Check if domain is trusted (supports wildcards)
756
+ // Check if URL is trusted (supports wildcards and paths)
707
757
  const isTrusted = trustedDomains.some(pattern =>
708
- domainMatchesPattern(domain, pattern)
758
+ urlMatchesPattern(urlInfo, pattern)
709
759
  )
710
760
 
711
761
  if (!isTrusted) {
712
762
  issues.push({
713
763
  type: 'domain',
714
- domain,
764
+ domain: urlInfo.hostname,
715
765
  reason,
716
766
  node,
717
767
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shopify/shop-minis-react",
3
3
  "license": "SEE LICENSE IN LICENSE.txt",
4
- "version": "0.4.15",
4
+ "version": "0.4.16",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "engines": {