shopify-cli 1.14.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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