shopify-cli 1.14.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/CONTRIBUTING.md +7 -7
  4. data/.github/DESIGN.md +3 -3
  5. data/.github/workflows/build.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/.rubocop.yml +3 -1
  8. data/.ruby-version +1 -1
  9. data/CHANGELOG.md +35 -29
  10. data/Gemfile +4 -0
  11. data/Gemfile.lock +32 -0
  12. data/LICENSE +4 -1
  13. data/README.md +92 -26
  14. data/RELEASING.md +29 -7
  15. data/Rakefile +2 -2
  16. data/SECURITY.md +1 -1
  17. data/bin/load_shopify.rb +1 -1
  18. data/bin/shopify +3 -3
  19. data/dev.yml +1 -1
  20. data/docs/app/node/index.md +1 -1
  21. data/docs/app/rails/index.md +1 -1
  22. data/docs/core/index.md +1 -1
  23. data/docs/getting-started/index.md +1 -1
  24. data/docs/getting-started/install/index.md +1 -1
  25. data/docs/getting-started/migrate/index.md +1 -1
  26. data/docs/getting-started/uninstall/index.md +1 -1
  27. data/docs/getting-started/upgrade/index.md +1 -1
  28. data/docs/help/start-app/index.md +1 -1
  29. data/docs/index.md +1 -1
  30. data/ext/shopify-cli/extconf.rb +17 -5
  31. data/install.sh +1 -1
  32. data/lib/docgen/index_template.md.erb +2 -2
  33. data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
  34. data/lib/graphql/find_organization.graphql +2 -1
  35. data/lib/project_types/extension/cli.rb +18 -15
  36. data/lib/project_types/extension/commands/build.rb +4 -5
  37. data/lib/project_types/extension/commands/connect.rb +35 -0
  38. data/lib/project_types/extension/commands/create.rb +12 -16
  39. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  40. data/lib/project_types/extension/commands/info.rb +86 -0
  41. data/lib/project_types/extension/commands/push.rb +8 -7
  42. data/lib/project_types/extension/commands/register.rb +4 -5
  43. data/lib/project_types/extension/commands/serve.rb +5 -8
  44. data/lib/project_types/extension/commands/tunnel.rb +3 -1
  45. data/lib/project_types/extension/errors.rb +9 -0
  46. data/lib/project_types/extension/extension_project.rb +5 -0
  47. data/lib/project_types/extension/features/argo.rb +6 -6
  48. data/lib/project_types/extension/features/argo_runtime.rb +22 -66
  49. data/lib/project_types/extension/features/argo_serve.rb +25 -18
  50. data/lib/project_types/extension/forms/connect.rb +42 -0
  51. data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
  52. data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
  53. data/lib/project_types/extension/messages/messages.rb +75 -11
  54. data/lib/project_types/extension/models/specification.rb +1 -0
  55. data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
  56. data/lib/project_types/extension/models/specification_handlers/default.rb +13 -3
  57. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +86 -0
  58. data/lib/project_types/extension/models/specifications.rb +1 -0
  59. data/lib/project_types/extension/tasks/configure_features.rb +6 -7
  60. data/lib/project_types/extension/tasks/configure_options.rb +20 -0
  61. data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
  62. data/lib/project_types/node/cli.rb +9 -21
  63. data/lib/project_types/node/commands/connect.rb +8 -2
  64. data/lib/project_types/node/commands/create.rb +9 -5
  65. data/lib/project_types/node/commands/deploy.rb +15 -5
  66. data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
  67. data/lib/project_types/node/commands/generate.rb +4 -2
  68. data/lib/project_types/node/commands/open.rb +4 -2
  69. data/lib/project_types/node/commands/serve.rb +3 -2
  70. data/lib/project_types/node/commands/tunnel.rb +4 -2
  71. data/lib/project_types/node/messages/messages.rb +46 -89
  72. data/lib/project_types/rails/cli.rb +9 -21
  73. data/lib/project_types/rails/commands/connect.rb +8 -2
  74. data/lib/project_types/rails/commands/create.rb +10 -6
  75. data/lib/project_types/rails/commands/deploy.rb +15 -5
  76. data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
  77. data/lib/project_types/rails/commands/generate.rb +15 -5
  78. data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
  79. data/lib/project_types/rails/commands/open.rb +4 -2
  80. data/lib/project_types/rails/commands/serve.rb +3 -2
  81. data/lib/project_types/rails/commands/tunnel.rb +4 -2
  82. data/lib/project_types/rails/messages/messages.rb +54 -101
  83. data/lib/project_types/script/cli.rb +5 -7
  84. data/lib/project_types/script/commands/create.rb +3 -1
  85. data/lib/project_types/script/commands/push.rb +4 -2
  86. data/lib/project_types/script/messages/messages.rb +52 -45
  87. data/lib/project_types/script/ui/error_handler.rb +2 -2
  88. data/lib/project_types/theme/cli.rb +15 -27
  89. data/lib/project_types/theme/commands/check.rb +33 -0
  90. data/lib/project_types/theme/commands/delete.rb +64 -0
  91. data/lib/project_types/theme/commands/language_server.rb +16 -0
  92. data/lib/project_types/theme/commands/package.rb +55 -0
  93. data/lib/project_types/theme/commands/publish.rb +43 -0
  94. data/lib/project_types/theme/commands/pull.rb +51 -0
  95. data/lib/project_types/theme/commands/push.rb +58 -32
  96. data/lib/project_types/theme/commands/serve.rb +7 -17
  97. data/lib/project_types/theme/forms/confirm_store.rb +15 -0
  98. data/lib/project_types/theme/forms/select.rb +59 -0
  99. data/lib/project_types/theme/messages/messages.rb +110 -106
  100. data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
  101. data/lib/shopify-cli/admin_api.rb +53 -38
  102. data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
  103. data/lib/shopify-cli/admin_api/schema.rb +1 -10
  104. data/lib/shopify-cli/api.rb +29 -14
  105. data/lib/shopify-cli/command.rb +15 -3
  106. data/lib/shopify-cli/commands.rb +7 -2
  107. data/lib/shopify-cli/commands/help.rb +2 -29
  108. data/lib/shopify-cli/commands/login.rb +95 -0
  109. data/lib/shopify-cli/commands/logout.rb +24 -8
  110. data/lib/shopify-cli/commands/populate.rb +23 -0
  111. data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
  112. data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
  113. data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
  114. data/lib/shopify-cli/commands/store.rb +15 -0
  115. data/lib/shopify-cli/commands/switch.rb +39 -0
  116. data/lib/shopify-cli/commands/system.rb +12 -0
  117. data/lib/shopify-cli/commands/whoami.rb +28 -0
  118. data/lib/shopify-cli/connect.rb +32 -0
  119. data/lib/shopify-cli/context.rb +52 -4
  120. data/lib/shopify-cli/core/entry_point.rb +3 -22
  121. data/lib/shopify-cli/db.rb +4 -4
  122. data/lib/shopify-cli/http_request.rb +10 -0
  123. data/lib/shopify-cli/identity_auth.rb +282 -0
  124. data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
  125. data/lib/shopify-cli/messages/messages.rb +132 -39
  126. data/lib/shopify-cli/partners_api.rb +21 -44
  127. data/lib/shopify-cli/partners_api/organizations.rb +8 -0
  128. data/lib/shopify-cli/project_commands.rb +16 -0
  129. data/lib/shopify-cli/project_type.rb +0 -31
  130. data/lib/shopify-cli/shopifolk.rb +8 -11
  131. data/lib/shopify-cli/sub_command.rb +1 -0
  132. data/lib/shopify-cli/tasks.rb +3 -0
  133. data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
  134. data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
  135. data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
  136. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
  137. data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
  138. data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
  139. data/lib/shopify-cli/theme/dev_server.rb +98 -0
  140. data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
  141. data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
  142. data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
  143. data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
  144. data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
  145. data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
  146. data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
  147. data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
  148. data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
  149. data/lib/shopify-cli/theme/development_theme.rb +69 -0
  150. data/lib/shopify-cli/theme/file.rb +112 -0
  151. data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
  152. data/lib/shopify-cli/theme/mime_type.rb +34 -0
  153. data/lib/shopify-cli/theme/syncer.rb +328 -0
  154. data/lib/shopify-cli/theme/theme.rb +204 -0
  155. data/lib/shopify-cli/version.rb +1 -1
  156. data/lib/shopify_cli.rb +18 -11
  157. data/shopify-cli.gemspec +12 -5
  158. data/shopify.fish +1 -1
  159. data/shopify.sh +1 -1
  160. metadata +88 -34
  161. data/.github/workflows/release.yml +0 -59
  162. data/lib/project_types/extension/features/argo_serve_options.rb +0 -42
  163. data/lib/project_types/node/commands/populate.rb +0 -23
  164. data/lib/project_types/rails/commands/populate.rb +0 -23
  165. data/lib/project_types/rails/commands/populate/customer.rb +0 -31
  166. data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
  167. data/lib/project_types/rails/commands/populate/product.rb +0 -30
  168. data/lib/project_types/theme/commands/connect.rb +0 -54
  169. data/lib/project_types/theme/commands/create.rb +0 -48
  170. data/lib/project_types/theme/commands/deploy.rb +0 -38
  171. data/lib/project_types/theme/commands/generate.rb +0 -20
  172. data/lib/project_types/theme/commands/generate/env.rb +0 -79
  173. data/lib/project_types/theme/forms/connect.rb +0 -34
  174. data/lib/project_types/theme/forms/create.rb +0 -22
  175. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
  176. data/lib/project_types/theme/themekit.rb +0 -113
  177. data/lib/shopify-cli/commands/connect.rb +0 -64
  178. data/lib/shopify-cli/commands/create.rb +0 -50
  179. data/lib/shopify-cli/oauth.rb +0 -198
@@ -1,6 +1,6 @@
1
1
  require "shopify_cli"
2
2
 
3
- module Node
3
+ module ShopifyCli
4
4
  module Commands
5
5
  class Populate
6
6
  class Customer < ShopifyCli::AdminAPI::PopulateResourceCommand
@@ -17,13 +17,7 @@ module Node
17
17
  def message(data)
18
18
  ret = data["customerCreate"]["customer"]
19
19
  id = ShopifyCli::API.gid_to_id(ret["id"])
20
- @ctx.message(
21
- "node.populate.customer.added",
22
- ret["displayName"],
23
- ShopifyCli::Project.current.env.shop,
24
- admin_url,
25
- id
26
- )
20
+ @ctx.message("core.populate.customer.added", ret["displayName"], @shop, admin_url, id)
27
21
  end
28
22
  end
29
23
  end
@@ -1,6 +1,6 @@
1
1
  require "shopify_cli"
2
2
 
3
- module Node
3
+ module ShopifyCli
4
4
  module Commands
5
5
  class Populate
6
6
  class DraftOrder < ShopifyCli::AdminAPI::PopulateResourceCommand
@@ -20,7 +20,7 @@ module Node
20
20
  def message(data)
21
21
  ret = data["draftOrderCreate"]["draftOrder"]
22
22
  id = ShopifyCli::API.gid_to_id(ret["id"])
23
- @ctx.message("node.populate.draft_order.added", ShopifyCli::Project.current.env.shop, admin_url, id)
23
+ @ctx.message("core.populate.draft_order.added", @shop, admin_url, id)
24
24
  end
25
25
  end
26
26
  end
@@ -1,6 +1,6 @@
1
1
  require "shopify_cli"
2
2
 
3
- module Node
3
+ module ShopifyCli
4
4
  module Commands
5
5
  class Populate
6
6
  class Product < ShopifyCli::AdminAPI::PopulateResourceCommand
@@ -16,13 +16,7 @@ module Node
16
16
  def message(data)
17
17
  ret = data["productCreate"]["product"]
18
18
  id = ShopifyCli::API.gid_to_id(ret["id"])
19
- @ctx.message(
20
- "node.populate.product.added",
21
- ret["title"],
22
- ShopifyCli::Project.current.env.shop,
23
- admin_url,
24
- id
25
- )
19
+ @ctx.message("core.populate.product.added", ret["title"], @shop, admin_url, id)
26
20
  end
27
21
  end
28
22
  end
@@ -0,0 +1,15 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Commands
5
+ class Store < ShopifyCli::Command
6
+ def call(_args, _name)
7
+ @ctx.puts(@ctx.message("core.store.shop", ShopifyCli::AdminAPI.get_shop_or_abort(@ctx)))
8
+ end
9
+
10
+ def self.help
11
+ ShopifyCli::Context.message("core.store.help", ShopifyCli::TOOL_NAME)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Commands
5
+ class Switch < ShopifyCli::Command
6
+ options do |parser, flags|
7
+ parser.on("--store=STORE") { |url| flags[:shop] = url }
8
+ # backwards compatibility allow 'shop' for now
9
+ parser.on("--shop=SHOP") { |url| flags[:shop] = url }
10
+ end
11
+
12
+ def call(*)
13
+ if Shopifolk.acting_as_shopify_organization?
14
+ @ctx.puts(@ctx.message("core.switch.disabled_as_shopify_org"))
15
+ return
16
+ end
17
+
18
+ shop = if options.flags[:shop]
19
+ Login.validate_shop(options.flags[:shop])
20
+ elsif (org_id = DB.get(:organization_id))
21
+ res = ShopifyCli::Tasks::SelectOrgAndShop.call(@ctx, organization_id: org_id)
22
+ res[:shop_domain]
23
+ else
24
+ AdminAPI.get_shop_or_abort(@ctx)
25
+ res = ShopifyCli::Tasks::SelectOrgAndShop.call(@ctx)
26
+ res[:shop_domain]
27
+ end
28
+ DB.set(shop: shop)
29
+ IdentityAuth.new(ctx: @ctx).reauthenticate
30
+
31
+ @ctx.puts(@ctx.message("core.switch.success", shop))
32
+ end
33
+
34
+ def self.help
35
+ ShopifyCli::Context.message("core.switch.help", ShopifyCli::TOOL_NAME)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -20,6 +20,7 @@ module ShopifyCli
20
20
  display_environment if show_all_details
21
21
 
22
22
  display_cli_constants(show_all_details)
23
+ display_shopify_store(show_all_details)
23
24
  display_cli_ruby(show_all_details)
24
25
  display_utility_commands(show_all_details)
25
26
  display_project_commands(show_all_details)
@@ -59,6 +60,17 @@ module ShopifyCli
59
60
  end
60
61
  end
61
62
 
63
+ def display_shopify_store(_show_all_details)
64
+ shop = if ShopifyCli::DB.exists?(:shop)
65
+ ShopifyCli::AdminAPI.get_shop_or_abort(@ctx)
66
+ else
67
+ @ctx.message("core.populate.error.no_shop", ShopifyCli::TOOL_NAME)
68
+ end
69
+
70
+ @ctx.puts("\n" + @ctx.message("core.system.shop_header"))
71
+ @ctx.puts(" " + shop)
72
+ end
73
+
62
74
  def display_cli_ruby(_show_all_details)
63
75
  rbconfig_constants = %w(host RUBY_VERSION_NAME)
64
76
 
@@ -0,0 +1,28 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Commands
5
+ class Whoami < ShopifyCli::Command
6
+ def call(_args, _name)
7
+ shop = ShopifyCli::DB.get(:shop)
8
+ org_id = ShopifyCli::DB.get(:organization_id)
9
+ org = ShopifyCli::PartnersAPI::Organizations.fetch(@ctx, id: org_id) unless org_id.nil?
10
+
11
+ output = if shop.nil? && org.nil?
12
+ @ctx.message("core.whoami.not_logged_in", ShopifyCli::TOOL_NAME)
13
+ elsif !shop.nil? && org.nil?
14
+ @ctx.message("core.whoami.logged_in_shop_only", shop)
15
+ elsif shop.nil? && !org.nil?
16
+ @ctx.message("core.whoami.logged_in_partner_only", org["businessName"])
17
+ else
18
+ @ctx.message("core.whoami.logged_in_partner_and_shop", shop, org["businessName"])
19
+ end
20
+ @ctx.puts(output)
21
+ end
22
+
23
+ def self.help
24
+ ShopifyCli::Context.message("core.whoami.help", ShopifyCli::TOOL_NAME)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ class Connect
5
+ def initialize(ctx)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def default_connect(project_type)
10
+ if Project.current&.env
11
+ @ctx.puts(@ctx.message("core.connect.already_connected_warning"))
12
+ end
13
+ org = ShopifyCli::Tasks::EnsureEnv.call(@ctx, regenerate: true)
14
+ write_cli_yml(project_type, org["id"]) unless Project.has_current?
15
+ api_key = Project.current(force_reload: true).env["api_key"]
16
+ get_app(org["apps"], api_key).first["title"]
17
+ end
18
+
19
+ def write_cli_yml(project_type, org_id)
20
+ ShopifyCli::Project.write(
21
+ @ctx,
22
+ project_type: project_type,
23
+ organization_id: org_id,
24
+ )
25
+ @ctx.done(@ctx.message("core.connect.cli_yml_saved"))
26
+ end
27
+
28
+ def get_app(apps, api_key)
29
+ apps.select { |app| app["apiKey"] == api_key }
30
+ end
31
+ end
32
+ end
@@ -61,9 +61,10 @@ module ShopifyCli
61
61
  # will return which operating system that the cli is running on [:mac, :linux]
62
62
  def os
63
63
  host = uname
64
- return :mac if /darwin/.match(host)
65
- return :linux if /linux/.match(host)
66
- return :windows if /mingw32/.match(host)
64
+ return :mac if /darwin/i.match(host)
65
+ return :windows if /mswin|mingw|cygwin/i.match(host)
66
+ return :linux if /linux|bsd/i.match(host)
67
+ :unknown
67
68
  end
68
69
 
69
70
  # will return true if the cli is running on an apple computer.
@@ -81,6 +82,16 @@ module ShopifyCli
81
82
  os == :windows
82
83
  end
83
84
 
85
+ # will return true if the os is unknown
86
+ def unknown_os?
87
+ os == :unknown
88
+ end
89
+
90
+ # will return true if being launched from a tty
91
+ def tty?
92
+ $stdin.tty? && !testing?
93
+ end
94
+
84
95
  # will return true if the cli is being run from an installation, and not a
85
96
  # development instance. The gem installation will not have a 'test' directory.
86
97
  # See `#development?` for checking for development environment.
@@ -106,6 +117,12 @@ module ShopifyCli
106
117
  ENV["CI"]
107
118
  end
108
119
 
120
+ ##
121
+ # will return true if the cli is running with the DEBUG flag
122
+ def debug?
123
+ getenv("DEBUG")
124
+ end
125
+
109
126
  # get a environment variable value by name.
110
127
  #
111
128
  # #### Parameters
@@ -299,6 +316,28 @@ module ShopifyCli
299
316
  puts(help)
300
317
  end
301
318
 
319
+ # will output to the console a link for the user to either copy/paste
320
+ # or click on.
321
+ #
322
+ # #### Parameters
323
+ # * `uri` - a http URI to open in a browser
324
+ #
325
+ def open_browser_url!(uri)
326
+ if tty?
327
+ if linux? && which("xdg-open")
328
+ system("xdg-open", uri.to_s)
329
+ elsif windows?
330
+ system("start", uri.to_s)
331
+ elsif mac?
332
+ system("open", uri.to_s)
333
+ else
334
+ open_url!(uri)
335
+ end
336
+ else
337
+ open_url!(uri)
338
+ end
339
+ end
340
+
302
341
  # will output a message, prefixed by a yellow star, indicating that task
303
342
  # started.
304
343
  #
@@ -318,6 +357,15 @@ module ShopifyCli
318
357
  Kernel.puts(CLI::UI.fmt(*args))
319
358
  end
320
359
 
360
+ # a wrapper around Kernel.warn to allow for easy formatting
361
+ #
362
+ # #### Parameters
363
+ # * `text` - a string message to output
364
+ #
365
+ def warn(*args)
366
+ Kernel.warn(CLI::UI.fmt(*args))
367
+ end
368
+
321
369
  # outputs a message, prefixed by a checkmark indicating that something completed
322
370
  #
323
371
  # #### Parameters
@@ -344,7 +392,7 @@ module ShopifyCli
344
392
  # * `text` - a string message to output
345
393
  #
346
394
  def debug(text)
347
- puts("{{red:DEBUG}} #{text}") if getenv("DEBUG")
395
+ puts("{{red:DEBUG}} #{text}") if debug?
348
396
  end
349
397
 
350
398
  # proxy call to Context.message.
@@ -5,35 +5,16 @@ module ShopifyCli
5
5
  module EntryPoint
6
6
  class << self
7
7
  def call(args, ctx = Context.new)
8
- # Check if the shim is set up by checking whether the old Finalizer FD exists
9
- begin
10
- is_shell_shim = false
11
- IO.open(9) { is_shell_shim = true }
12
- rescue Errno::EBADF
13
- # This is expected if the descriptor doesn't exist
14
- rescue ArgumentError => e
15
- # This can happen on RVM, because it can use fd 9 itself and block access to it. That only happens if the fd
16
- # did not exist beforehand, so that means there was no fd 9 before Ruby started.
17
- unless e.message == "The given fd is not accessible because RubyVM reserves it"
18
- raise e
19
- end
20
- end
21
-
22
- if !ctx.testing? && is_shell_shim
23
- ctx.puts(ctx.message("core.warning.shell_shim"))
24
- return
25
- end
26
-
27
8
  if ctx.development?
28
- ctx.puts(
9
+ ctx.warn(
29
10
  ctx.message("core.warning.development_version", File.join(ShopifyCli::ROOT, "bin", ShopifyCli::TOOL_NAME))
30
11
  )
31
12
  elsif !ctx.testing?
32
13
  new_version = ctx.new_version
33
- ctx.puts(ctx.message("core.warning.new_version", ShopifyCli::VERSION, new_version)) unless new_version.nil?
14
+ ctx.warn(ctx.message("core.warning.new_version", ShopifyCli::VERSION, new_version)) unless new_version.nil?
34
15
  end
35
16
 
36
- ProjectType.load_type(Project.current_project_type)
17
+ ProjectType.load_all
37
18
 
38
19
  task_registry = ShopifyCli::Tasks::Registry
39
20
 
@@ -42,7 +42,7 @@ module ShopifyCli
42
42
  #
43
43
  # #### Usage
44
44
  #
45
- # exists = ShopifyCli::DB.exists?('admin_access_token')
45
+ # exists = ShopifyCli::DB.exists?('shopify_exchange_token')
46
46
  #
47
47
  def exists?(key)
48
48
  db.transaction(true) { db.root?(key) }
@@ -55,7 +55,7 @@ module ShopifyCli
55
55
  #
56
56
  # #### Usage
57
57
  #
58
- # ShopifyCli::DB.set(admin_access_token: 'token', metric_consent: true)
58
+ # ShopifyCli::DB.set(shopify_exchange_token: 'token', metric_consent: true)
59
59
  #
60
60
  def set(**args)
61
61
  db.transaction do
@@ -80,7 +80,7 @@ module ShopifyCli
80
80
  #
81
81
  # #### Usage
82
82
  #
83
- # ShopifyCli::DB.get(:admin_access_token)
83
+ # ShopifyCli::DB.get(:shopify_exchange_token)
84
84
  #
85
85
  def get(key)
86
86
  val = db.transaction(true) { db[key] }
@@ -95,7 +95,7 @@ module ShopifyCli
95
95
  #
96
96
  # #### Usage
97
97
  #
98
- # ShopifyCli::DB.del(:admin_access_token)
98
+ # ShopifyCli::DB.del(:shopify_exchange_token)
99
99
  #
100
100
  def del(*args)
101
101
  db.transaction { args.each { |key| db.delete(key) } }
@@ -8,11 +8,21 @@ module ShopifyCli
8
8
  request(uri, body, headers, req)
9
9
  end
10
10
 
11
+ def put(uri, body, headers)
12
+ req = ::Net::HTTP::Put.new(uri.request_uri)
13
+ request(uri, body, headers, req)
14
+ end
15
+
11
16
  def get(uri, body, headers)
12
17
  req = ::Net::HTTP::Get.new(uri.request_uri)
13
18
  request(uri, body, headers, req)
14
19
  end
15
20
 
21
+ def delete(uri, body, headers)
22
+ req = ::Net::HTTP::Delete.new(uri.request_uri)
23
+ request(uri, body, headers, req)
24
+ end
25
+
16
26
  def request(uri, body, headers, req)
17
27
  http = ::Net::HTTP.new(uri.host, uri.port)
18
28
  http.use_ssl = true
@@ -0,0 +1,282 @@
1
+ require "base64"
2
+ require "digest"
3
+ require "json"
4
+ require "net/http"
5
+ require "securerandom"
6
+ require "openssl"
7
+ require "shopify_cli"
8
+ require "uri"
9
+ require "webrick"
10
+
11
+ module ShopifyCli
12
+ class IdentityAuth
13
+ include SmartProperties
14
+
15
+ autoload :Servlet, "shopify-cli/identity_auth/servlet"
16
+
17
+ class Error < StandardError; end
18
+ class Timeout < StandardError; end
19
+ LocalRequest = Struct.new(:method, :path, :query, :protocol)
20
+ LOCAL_DEBUG = "SHOPIFY_APP_CLI_LOCAL_PARTNERS"
21
+
22
+ DEFAULT_PORT = 3456
23
+ REDIRECT_HOST = "http://127.0.0.1:#{DEFAULT_PORT}"
24
+
25
+ APPLICATION_SCOPES = {
26
+ "shopify" => %w[https://api.shopify.com/auth/shop.admin.graphql https://api.shopify.com/auth/shop.admin.themes https://api.shopify.com/auth/partners.collaborator-relationships.readonly],
27
+ "storefront_renderer_production" => %w[https://api.shopify.com/auth/shop.storefront-renderer.devtools],
28
+ "partners" => %w[https://api.shopify.com/auth/partners.app.cli.access],
29
+ }
30
+
31
+ APPLICATION_CLIENT_IDS = {
32
+ "shopify" => "7ee65a63608843c577db8b23c4d7316ea0a01bd2f7594f8a9c06ea668c1b775c",
33
+ "storefront_renderer_production" => "ee139b3d-5861-4d45-b387-1bc3ada7811c",
34
+ "partners" => "271e16d403dfa18082ffb3d197bd2b5f4479c3fc32736d69296829cbb28d41a6",
35
+ }
36
+
37
+ DEV_APPLICATION_CLIENT_IDS = {
38
+ "shopify" => "e92482cebb9bfb9fb5a0199cc770fde3de6c8d16b798ee73e36c9d815e070e52",
39
+ "storefront_renderer_production" => "46f603de-894f-488d-9471-5b721280ff49",
40
+ "partners" => "df89d73339ac3c6c5f0a98d9ca93260763e384d51d6038da129889c308973978",
41
+ }
42
+
43
+ EXCHANGE_TOKENS = APPLICATION_SCOPES.keys.map do |key|
44
+ "#{key}_exchange_token".to_sym
45
+ end
46
+
47
+ IDENTITY_ACCESS_TOKENS = %i[
48
+ identity_access_token
49
+ identity_refresh_token
50
+ ]
51
+
52
+ property! :ctx
53
+ property :store, default: -> { ShopifyCli::DB.new }
54
+ property :state_token, accepts: String, default: SecureRandom.hex(30)
55
+ property :code_verifier, accepts: String, default: SecureRandom.hex(30)
56
+
57
+ attr_accessor :response_query
58
+
59
+ def authenticate
60
+ return if refresh_exchange_tokens || refresh_access_tokens
61
+
62
+ initiate_authentication
63
+
64
+ begin
65
+ request_access_token(code: receive_access_code)
66
+ rescue IdentityAuth::Timeout => e
67
+ ctx.abort(e.message)
68
+ end
69
+ request_exchange_tokens
70
+ end
71
+
72
+ def reauthenticate
73
+ return if refresh_exchange_tokens || refresh_access_tokens
74
+ ctx.abort(ctx.message("core.identity_auth.error.reauthenticate", ShopifyCli::TOOL_NAME))
75
+ end
76
+
77
+ def code_challenge
78
+ @code_challenge ||= Base64.urlsafe_encode64(
79
+ OpenSSL::Digest::SHA256.digest(code_verifier),
80
+ padding: false,
81
+ )
82
+ end
83
+
84
+ def server
85
+ @server ||= begin
86
+ server = WEBrick::HTTPServer.new(
87
+ Port: DEFAULT_PORT,
88
+ Logger: WEBrick::Log.new(File.open(File::NULL, "w")),
89
+ AccessLog: [],
90
+ )
91
+ server.mount("/", Servlet, self, state_token)
92
+ server
93
+ end
94
+ end
95
+
96
+ def self.delete_tokens_and_keys
97
+ ShopifyCli::DB.del(*IDENTITY_ACCESS_TOKENS)
98
+ ShopifyCli::DB.del(*EXCHANGE_TOKENS)
99
+ end
100
+
101
+ private
102
+
103
+ def initiate_authentication
104
+ @server_thread = Thread.new { server.start }
105
+ params = {
106
+ client_id: client_id,
107
+ scope: scopes(APPLICATION_SCOPES.values.flatten),
108
+ redirect_uri: REDIRECT_HOST,
109
+ state: state_token,
110
+ response_type: :code,
111
+ }
112
+ params.merge!(challange_params)
113
+ uri = URI.parse("#{auth_url}/authorize")
114
+ uri.query = URI.encode_www_form(params)
115
+ open_browser_authentication(uri)
116
+ end
117
+
118
+ def open_browser_authentication(uri)
119
+ ctx.open_browser_url!(uri)
120
+ end
121
+
122
+ def receive_access_code
123
+ @access_code ||= begin
124
+ @server_thread.join(240)
125
+ raise Timeout, ctx.message("core.identity_auth.error.timeout") if response_query.nil?
126
+ raise Error, response_query["error_description"] unless response_query["error"].nil?
127
+ response_query["code"]
128
+ end
129
+ end
130
+
131
+ def request_access_token(code:)
132
+ resp = post_token_request(
133
+ grant_type: :authorization_code,
134
+ code: code,
135
+ redirect_uri: REDIRECT_HOST,
136
+ client_id: client_id,
137
+ code_verifier: code_verifier,
138
+ )
139
+ store.set(
140
+ identity_access_token: resp["access_token"],
141
+ identity_refresh_token: resp["refresh_token"],
142
+ )
143
+ end
144
+
145
+ def refresh_access_tokens
146
+ return false unless IDENTITY_ACCESS_TOKENS.all? { |key| store.exists?(key) }
147
+
148
+ resp = post_token_request(
149
+ grant_type: :refresh_token,
150
+ access_token: store.get(:identity_access_token),
151
+ refresh_token: store.get(:identity_refresh_token),
152
+ client_id: client_id,
153
+ )
154
+ store.set(
155
+ identity_access_token: resp["access_token"],
156
+ identity_refresh_token: resp["refresh_token"],
157
+ )
158
+
159
+ # Need to refresh the exchange token on successful access token refresh
160
+ request_exchange_tokens
161
+
162
+ true
163
+ rescue
164
+ store.del(*IDENTITY_ACCESS_TOKENS)
165
+ false
166
+ end
167
+
168
+ def refresh_exchange_tokens
169
+ return false unless EXCHANGE_TOKENS.all? { |key| store.exists?(key) }
170
+
171
+ request_exchange_tokens
172
+
173
+ true
174
+ rescue
175
+ store.del(*EXCHANGE_TOKENS)
176
+ false
177
+ end
178
+
179
+ def request_exchange_tokens
180
+ APPLICATION_SCOPES.each do |key, scopes|
181
+ request_exchange_token(key, client_id_for_application(key), scopes)
182
+ end
183
+ end
184
+
185
+ def request_exchange_token(name, audience, additional_scopes)
186
+ return if name == "shopify" && !store.exists?(:shop)
187
+
188
+ params = {
189
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
190
+ requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
191
+ subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
192
+ client_id: client_id,
193
+ audience: audience,
194
+ scope: scopes(additional_scopes),
195
+ subject_token: store.get(:identity_access_token),
196
+ }.tap do |result|
197
+ if name == "shopify"
198
+ result[:destination] = "https://#{store.get(:shop)}/admin"
199
+ end
200
+ end
201
+ # ctx.debug(params)
202
+ resp = post_token_request(params)
203
+ store.set("#{name}_exchange_token".to_sym => resp["access_token"])
204
+ ctx.debug("#{name}_exchange_token: " + resp["access_token"])
205
+ end
206
+
207
+ def post_token_request(params)
208
+ post_request("/token", params)
209
+ end
210
+
211
+ def post_request(endpoint, params)
212
+ uri = URI.parse("#{auth_url}#{endpoint}")
213
+ https = Net::HTTP.new(uri.host, uri.port)
214
+ https.use_ssl = true
215
+ request = Net::HTTP::Post.new(uri.path)
216
+ request["User-Agent"] = "Shopify CLI #{::ShopifyCli::VERSION}"
217
+ request.body = URI.encode_www_form(params)
218
+ res = https.request(request)
219
+ unless res.is_a?(Net::HTTPSuccess)
220
+ error_msg = JSON.parse(res.body)["error_description"]
221
+ shop = store.get(:shop)
222
+ if error_msg.include?("destination")
223
+ store.del(:shop)
224
+ ctx.abort(ctx.message("core.identity_auth.error.invalid_destination", shop))
225
+ end
226
+ raise Error, error_msg
227
+ end
228
+ JSON.parse(res.body)
229
+ end
230
+
231
+ def challange_params
232
+ {
233
+ code_challenge: code_challenge,
234
+ code_challenge_method: "S256",
235
+ }
236
+ end
237
+
238
+ def auth_url
239
+ return "https://accounts.shopify.com/oauth" if ENV[LOCAL_DEBUG].nil?
240
+ "https://identity.myshopify.io/oauth"
241
+ end
242
+
243
+ def client_id_for_application(application_name)
244
+ client_ids = if ENV[LOCAL_DEBUG]
245
+ DEV_APPLICATION_CLIENT_IDS
246
+ else
247
+ APPLICATION_CLIENT_IDS
248
+ end
249
+
250
+ client_ids[application_name]
251
+ end
252
+
253
+ def scopes(additional_scopes = [])
254
+ (["openid"] + additional_scopes).tap do |result|
255
+ result << "employee" if ShopifyCli::Shopifolk.acting_as_shopify_organization?
256
+ end.join(" ")
257
+ end
258
+
259
+ def client_id
260
+ return "fbdb2649-e327-4907-8f67-908d24cfd7e3" if ENV[LOCAL_DEBUG].nil?
261
+
262
+ ctx.abort(ctx.message("core.identity_auth.error.local_identity_not_running")) unless local_identity_running?
263
+
264
+ # Fetch the client ID from the local Identity Dynamic Registration endpoint
265
+ response = post_request("/client", {
266
+ name: "shopify-cli-development",
267
+ public_type: "native",
268
+ })
269
+
270
+ response["client_id"]
271
+ end
272
+
273
+ def local_identity_running?
274
+ Net::HTTP.start("identity.myshopify.io", 443, use_ssl: true, open_timeout: 1, read_timeout: 10) do |http|
275
+ req = Net::HTTP::Get.new(URI.join("https://identity.myshopify.io", "/services/ping"))
276
+ http.request(req).is_a?(Net::HTTPSuccess)
277
+ end
278
+ rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::EHOSTDOWN, Errno::EADDRNOTAVAIL, Errno::ECONNREFUSED
279
+ false
280
+ end
281
+ end
282
+ end