lex-github 0.2.4 → 0.3.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -3
  3. data/.rubocop.yml +2 -53
  4. data/CHANGELOG.md +55 -0
  5. data/CLAUDE.md +45 -19
  6. data/Gemfile +1 -0
  7. data/README.md +155 -83
  8. data/lex-github.gemspec +2 -0
  9. data/lib/legion/extensions/github/app/actor/token_refresh.rb +68 -0
  10. data/lib/legion/extensions/github/app/actor/webhook_poller.rb +65 -0
  11. data/lib/legion/extensions/github/app/hooks/setup.rb +19 -0
  12. data/lib/legion/extensions/github/app/hooks/webhook.rb +19 -0
  13. data/lib/legion/extensions/github/app/runners/auth.rb +48 -0
  14. data/lib/legion/extensions/github/app/runners/credential_store.rb +46 -0
  15. data/lib/legion/extensions/github/app/runners/installations.rb +56 -0
  16. data/lib/legion/extensions/github/app/runners/manifest.rb +65 -0
  17. data/lib/legion/extensions/github/app/runners/webhooks.rb +118 -0
  18. data/lib/legion/extensions/github/app/transport/exchanges/app.rb +17 -0
  19. data/lib/legion/extensions/github/app/transport/messages/event.rb +18 -0
  20. data/lib/legion/extensions/github/app/transport/queues/auth.rb +18 -0
  21. data/lib/legion/extensions/github/app/transport/queues/webhooks.rb +18 -0
  22. data/lib/legion/extensions/github/cli/app.rb +57 -0
  23. data/lib/legion/extensions/github/cli/auth.rb +99 -0
  24. data/lib/legion/extensions/github/client.rb +24 -0
  25. data/lib/legion/extensions/github/errors.rb +44 -0
  26. data/lib/legion/extensions/github/helpers/browser_auth.rb +106 -0
  27. data/lib/legion/extensions/github/helpers/cache.rb +99 -0
  28. data/lib/legion/extensions/github/helpers/callback_server.rb +89 -0
  29. data/lib/legion/extensions/github/helpers/client.rb +292 -2
  30. data/lib/legion/extensions/github/helpers/scope_registry.rb +91 -0
  31. data/lib/legion/extensions/github/helpers/token_cache.rb +86 -0
  32. data/lib/legion/extensions/github/middleware/credential_fallback.rb +76 -0
  33. data/lib/legion/extensions/github/middleware/rate_limit.rb +40 -0
  34. data/lib/legion/extensions/github/middleware/scope_probe.rb +37 -0
  35. data/lib/legion/extensions/github/oauth/actor/token_refresh.rb +76 -0
  36. data/lib/legion/extensions/github/oauth/hooks/callback.rb +19 -0
  37. data/lib/legion/extensions/github/oauth/runners/auth.rb +111 -0
  38. data/lib/legion/extensions/github/oauth/transport/exchanges/oauth.rb +17 -0
  39. data/lib/legion/extensions/github/oauth/transport/queues/auth.rb +18 -0
  40. data/lib/legion/extensions/github/runners/actions.rb +100 -0
  41. data/lib/legion/extensions/github/runners/branches.rb +8 -6
  42. data/lib/legion/extensions/github/runners/checks.rb +84 -0
  43. data/lib/legion/extensions/github/runners/comments.rb +15 -9
  44. data/lib/legion/extensions/github/runners/commits.rb +13 -8
  45. data/lib/legion/extensions/github/runners/contents.rb +6 -4
  46. data/lib/legion/extensions/github/runners/deployments.rb +76 -0
  47. data/lib/legion/extensions/github/runners/gists.rb +11 -6
  48. data/lib/legion/extensions/github/runners/issues.rb +18 -11
  49. data/lib/legion/extensions/github/runners/labels.rb +18 -11
  50. data/lib/legion/extensions/github/runners/organizations.rb +12 -10
  51. data/lib/legion/extensions/github/runners/pull_requests.rb +26 -16
  52. data/lib/legion/extensions/github/runners/releases.rb +89 -0
  53. data/lib/legion/extensions/github/runners/repositories.rb +19 -12
  54. data/lib/legion/extensions/github/runners/repository_webhooks.rb +76 -0
  55. data/lib/legion/extensions/github/runners/search.rb +13 -10
  56. data/lib/legion/extensions/github/runners/users.rb +14 -10
  57. data/lib/legion/extensions/github/version.rb +1 -1
  58. data/lib/legion/extensions/github.rb +23 -1
  59. metadata +63 -1
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Actor
8
+ class WebhookPoller < Legion::Extensions::Actors::Poll # rubocop:disable Legion/Extension/SelfContainedActorRunnerClass,Legion/Extension/EveryActorRequiresTime
9
+ def use_runner? = false
10
+ def check_subtask? = false
11
+ def generate_task? = false
12
+
13
+ def time
14
+ 60
15
+ end
16
+
17
+ # rubocop:disable Legion/Extension/ActorEnabledSideEffects
18
+ def enabled?
19
+ github_poll_settings[:owner] && github_poll_settings[:repo]
20
+ rescue StandardError => _e
21
+ false
22
+ end
23
+ # rubocop:enable Legion/Extension/ActorEnabledSideEffects
24
+
25
+ def manual
26
+ settings = github_poll_settings
27
+ owner = settings[:owner]
28
+ repo = settings[:repo]
29
+ return unless owner && repo
30
+
31
+ client = Legion::Extensions::Github::Client.new
32
+ return unless client.respond_to?(:list_events)
33
+
34
+ result = client.list_events(owner: owner, repo: repo)
35
+ events = result[:result]
36
+ return unless events.is_a?(Array)
37
+
38
+ events.each do |event|
39
+ publish_event(event)
40
+ end
41
+ rescue StandardError => e
42
+ log.error("App::Actor::WebhookPoller: #{e.message}")
43
+ end
44
+
45
+ private
46
+
47
+ def github_poll_settings
48
+ return {} unless defined?(Legion::Settings)
49
+
50
+ Legion::Settings[:github]&.dig(:webhook_poller) || {}
51
+ rescue StandardError => _e
52
+ {}
53
+ end
54
+
55
+ def publish_event(event)
56
+ Legion::Extensions::Github::App::Transport::Messages::Event.new(event).publish
57
+ rescue StandardError => e
58
+ log.warn("WebhookPoller#publish_event: #{e.message}")
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Hooks
8
+ class Setup < Legion::Extensions::Hooks::Base # rubocop:disable Legion/Extension/HookMissingRunnerClass
9
+ mount '/setup/callback'
10
+
11
+ def self.runner_class
12
+ 'Legion::Extensions::Github::App::Runners::Manifest'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Hooks
8
+ class Webhook < Legion::Extensions::Hooks::Base # rubocop:disable Legion/Extension/HookMissingRunnerClass
9
+ mount '/webhook'
10
+
11
+ def self.runner_class
12
+ 'Legion::Extensions::Github::App::Runners::Webhooks'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'openssl'
5
+ require 'legion/extensions/github/helpers/client'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Github
10
+ module App
11
+ module Runners
12
+ module Auth
13
+ include Legion::Extensions::Github::Helpers::Client
14
+
15
+ def generate_jwt(app_id:, private_key:, **)
16
+ key = OpenSSL::PKey::RSA.new(private_key)
17
+ now = Time.now.to_i
18
+ payload = { iat: now - 60, exp: now + (10 * 60), iss: app_id.to_s }
19
+ token = JWT.encode(payload, key, 'RS256')
20
+ { result: token }
21
+ end
22
+
23
+ def create_installation_token(jwt:, installation_id:, **)
24
+ conn = connection(token: jwt, **)
25
+ response = conn.post("/app/installations/#{installation_id}/access_tokens")
26
+ { result: response.body }
27
+ end
28
+
29
+ def list_installations(jwt:, per_page: 30, page: 1, **)
30
+ conn = connection(token: jwt, **)
31
+ response = conn.get('/app/installations', per_page: per_page, page: page)
32
+ { result: response.body }
33
+ end
34
+
35
+ def get_installation(jwt:, installation_id:, **)
36
+ conn = connection(token: jwt, **)
37
+ response = conn.get("/app/installations/#{installation_id}")
38
+ { result: response.body }
39
+ end
40
+
41
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
42
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Github
8
+ module App
9
+ module Runners
10
+ module CredentialStore
11
+ def store_app_credentials(app_id:, private_key:, client_id:, client_secret:, webhook_secret:, **)
12
+ vault_set('github/app/app_id', app_id)
13
+ vault_set('github/app/private_key', private_key)
14
+ vault_set('github/app/client_id', client_id)
15
+ vault_set('github/app/client_secret', client_secret)
16
+ vault_set('github/app/webhook_secret', webhook_secret)
17
+ { result: true }
18
+ end
19
+
20
+ def store_oauth_token(user:, access_token:, refresh_token:, expires_in: nil, scope: nil, **)
21
+ data = { 'access_token' => access_token, 'refresh_token' => refresh_token,
22
+ 'expires_in' => expires_in, 'scope' => scope,
23
+ 'stored_at' => Time.now.iso8601 }.compact
24
+ vault_set("github/oauth/#{user}/token", data)
25
+ # Also write to canonical delegated path so resolve_vault_delegated can discover the token
26
+ vault_set('github/oauth/delegated/token', data)
27
+ { result: true }
28
+ end
29
+
30
+ def load_oauth_token(user:, **)
31
+ data = begin
32
+ vault_get("github/oauth/#{user}/token")
33
+ rescue StandardError => _e
34
+ nil
35
+ end
36
+ { result: data }
37
+ end
38
+
39
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
40
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/github/helpers/client'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Github
8
+ module App
9
+ module Runners
10
+ module Installations
11
+ include Legion::Extensions::Github::Helpers::Client
12
+
13
+ def list_installations(jwt:, per_page: 30, page: 1, **)
14
+ conn = connection(token: jwt, **)
15
+ response = conn.get('/app/installations', per_page: per_page, page: page)
16
+ { result: response.body }
17
+ end
18
+
19
+ def get_installation(jwt:, installation_id:, **)
20
+ conn = connection(token: jwt, **)
21
+ response = conn.get("/app/installations/#{installation_id}")
22
+ { result: response.body }
23
+ end
24
+
25
+ def list_installation_repos(per_page: 30, page: 1, **)
26
+ response = connection(**).get('/installation/repositories',
27
+ per_page: per_page, page: page)
28
+ { result: response.body }
29
+ end
30
+
31
+ def suspend_installation(jwt:, installation_id:, **)
32
+ conn = connection(token: jwt, **)
33
+ response = conn.put("/app/installations/#{installation_id}/suspended")
34
+ { result: response.status == 204 }
35
+ end
36
+
37
+ def unsuspend_installation(jwt:, installation_id:, **)
38
+ conn = connection(token: jwt, **)
39
+ response = conn.delete("/app/installations/#{installation_id}/suspended")
40
+ { result: response.status == 204 }
41
+ end
42
+
43
+ def delete_installation(jwt:, installation_id:, **)
44
+ conn = connection(token: jwt, **)
45
+ response = conn.delete("/app/installations/#{installation_id}")
46
+ { result: response.status == 204 }
47
+ end
48
+
49
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
50
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uri'
5
+ require 'legion/extensions/github/helpers/client'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Github
10
+ module App
11
+ module Runners
12
+ module Manifest
13
+ include Legion::Extensions::Github::Helpers::Client
14
+
15
+ DEFAULT_PERMISSIONS = {
16
+ contents: 'write', issues: 'write', pull_requests: 'write',
17
+ metadata: 'read', administration: 'write', members: 'read',
18
+ checks: 'write', statuses: 'write', actions: 'read',
19
+ workflows: 'write', webhooks: 'write', repository_hooks: 'write'
20
+ }.freeze
21
+
22
+ DEFAULT_EVENTS = %w[
23
+ push pull_request pull_request_review issues issue_comment
24
+ create delete check_run check_suite status workflow_run
25
+ repository installation
26
+ ].freeze
27
+
28
+ def generate_manifest(name:, url:, webhook_url:, callback_url:,
29
+ permissions: DEFAULT_PERMISSIONS, events: DEFAULT_EVENTS,
30
+ public: true, **)
31
+ manifest = {
32
+ name: name, url: url, public: public,
33
+ hook_attributes: { url: webhook_url, active: true },
34
+ setup_url: callback_url,
35
+ redirect_url: callback_url,
36
+ default_permissions: permissions,
37
+ default_events: events
38
+ }
39
+ { result: manifest }
40
+ end
41
+
42
+ def exchange_manifest_code(code:, **)
43
+ conn = connection(**)
44
+ response = conn.post("/app-manifests/#{code}/conversions")
45
+ { result: response.body }
46
+ end
47
+
48
+ def manifest_url(manifest:, org: nil, **)
49
+ base = if org
50
+ "https://github.com/organizations/#{org}/settings/apps/new"
51
+ else
52
+ 'https://github.com/settings/apps/new'
53
+ end
54
+ json_str = ::JSON.generate(manifest)
55
+ { result: "#{base}?manifest=#{URI.encode_www_form_component(json_str)}" }
56
+ end
57
+
58
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
59
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'openssl'
5
+ require 'legion/extensions/github/helpers/client'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Github
10
+ module App
11
+ module Runners
12
+ module Webhooks
13
+ include Legion::Extensions::Github::Helpers::Client
14
+
15
+ def verify_signature(payload:, signature:, secret:, **)
16
+ return { result: false } if signature.nil? || signature.empty?
17
+
18
+ expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, payload)}"
19
+ # Use constant-time comparison to prevent timing side-channel attacks.
20
+ # Pad to equal length so fixed_length_secure_compare can be used safely.
21
+ result = expected.length == signature.length &&
22
+ OpenSSL.fixed_length_secure_compare(expected, signature)
23
+ { result: result }
24
+ end
25
+
26
+ def parse_event(payload:, event_type:, delivery_id:, **)
27
+ parsed = payload.is_a?(String) ? ::JSON.parse(payload) : payload
28
+ { result: { event_type: event_type, delivery_id: delivery_id, payload: parsed } }
29
+ end
30
+
31
+ def receive_event(payload:, signature:, secret:, event_type:, delivery_id:, **)
32
+ verified = verify_signature(payload: payload, signature: signature, secret: secret)[:result]
33
+ unless verified
34
+ return { result: { verified: false, event_type: event_type, delivery_id: delivery_id,
35
+ payload: nil } }
36
+ end
37
+
38
+ parsed = parse_event(payload: payload, event_type: event_type, delivery_id: delivery_id)[:result]
39
+ invalidate_scopes_for_event(event_type: event_type, payload: parsed[:payload])
40
+ { result: parsed.merge(verified: true) }
41
+ end
42
+
43
+ SCOPE_INVALIDATION_EVENTS = %w[installation installation_repositories].freeze
44
+
45
+ def invalidate_scopes_for_event(event_type:, payload:, **)
46
+ return unless SCOPE_INVALIDATION_EVENTS.include?(event_type.to_s)
47
+
48
+ owner = payload&.dig('installation', 'account', 'login')
49
+ return unless owner
50
+
51
+ invalidate_all_scopes_for_owner(owner: owner)
52
+ end
53
+
54
+ def invalidate_all_scopes_for_owner(owner:)
55
+ known_fingerprints = resolve_known_fingerprints
56
+ known_fingerprints.each do |fp|
57
+ invalidate_scope(fingerprint: fp, owner: owner)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def resolve_known_fingerprints
64
+ fingerprints = []
65
+
66
+ # Delegated (OAuth user) — check Vault and settings without resolving tokens
67
+ fingerprints << credential_fingerprint(auth_type: :oauth_user, identifier: 'vault_delegated') if vault_delegated_configured?
68
+ fingerprints << credential_fingerprint(auth_type: :oauth_user, identifier: 'settings_delegated') if settings_delegated_configured?
69
+
70
+ # App installation — derive from app_id without generating installation tokens
71
+ if (vault_app_id = safe_vault_get('github/app/app_id'))
72
+ fingerprints << credential_fingerprint(auth_type: :app_installation, identifier: "vault_app_#{vault_app_id}")
73
+ end
74
+ if (settings_app_id = safe_settings_dig(:github, :app, :app_id))
75
+ fingerprints << credential_fingerprint(auth_type: :app_installation, identifier: "settings_app_#{settings_app_id}")
76
+ end
77
+
78
+ # PAT
79
+ fingerprints << credential_fingerprint(auth_type: :pat, identifier: 'vault_pat') if safe_vault_get('github/token')
80
+ fingerprints << credential_fingerprint(auth_type: :pat, identifier: 'settings_pat') if safe_settings_dig(:github, :token)
81
+
82
+ # CLI and ENV
83
+ fingerprints << credential_fingerprint(auth_type: :cli, identifier: 'gh_cli')
84
+ fingerprints << credential_fingerprint(auth_type: :env, identifier: 'env')
85
+
86
+ fingerprints.uniq
87
+ rescue StandardError => _e
88
+ []
89
+ end
90
+
91
+ def vault_delegated_configured?
92
+ defined?(Legion::Crypt) && safe_vault_get('github/oauth/delegated/token')
93
+ end
94
+
95
+ def settings_delegated_configured?
96
+ defined?(Legion::Settings) && safe_settings_dig(:github, :oauth, :access_token)
97
+ end
98
+
99
+ def safe_vault_get(path)
100
+ vault_get(path) if defined?(Legion::Crypt)
101
+ rescue StandardError => _e
102
+ nil
103
+ end
104
+
105
+ def safe_settings_dig(*keys)
106
+ Legion::Settings.dig(*keys) if defined?(Legion::Settings)
107
+ rescue StandardError => _e
108
+ nil
109
+ end
110
+
111
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
112
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Transport
8
+ module Exchanges
9
+ class App < Legion::Transport::Exchange
10
+ def exchange_name = 'lex.github.app'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Transport
8
+ module Messages
9
+ class Event < Legion::Transport::Message
10
+ def routing_key = 'lex.github.app.runners.webhooks'
11
+ def exchange = Legion::Extensions::Github::App::Transport::Exchanges::App
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Transport
8
+ module Queues
9
+ class Auth < Legion::Transport::Queue
10
+ def queue_name = 'lex.github.app.runners.auth'
11
+ def queue_options = { auto_delete: false }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Transport
8
+ module Queues
9
+ class Webhooks < Legion::Transport::Queue
10
+ def queue_name = 'lex.github.app.runners.webhooks'
11
+ def queue_options = { auto_delete: false }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/github/helpers/client'
4
+ require 'legion/extensions/github/helpers/callback_server'
5
+ require 'legion/extensions/github/app/runners/manifest'
6
+ require 'legion/extensions/github/app/runners/credential_store'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Github
11
+ module CLI
12
+ module App
13
+ include Helpers::Client
14
+ include Github::App::Runners::Manifest
15
+ include Github::App::Runners::CredentialStore
16
+
17
+ def setup(name:, url:, webhook_url:, org: nil, callback_timeout: 300, **)
18
+ server = Helpers::CallbackServer.new
19
+ server.start
20
+ callback_url = server.redirect_uri
21
+
22
+ manifest = generate_manifest(
23
+ name: name, url: url,
24
+ webhook_url: webhook_url,
25
+ callback_url: callback_url
26
+ )[:result]
27
+
28
+ url_result = manifest_url(manifest: manifest, org: org)[:result]
29
+
30
+ { result: { manifest_url: url_result, callback_port: server.port,
31
+ message: 'Open the manifest URL in your browser to create the GitHub App',
32
+ callback: server.wait_for_callback(timeout: callback_timeout) } }
33
+ ensure
34
+ server&.shutdown
35
+ end
36
+
37
+ def complete_setup(code:, **)
38
+ result = exchange_manifest_code(code: code)[:result]
39
+ return { error: 'exchange_failed' } unless result&.dig('id')
40
+
41
+ if respond_to?(:store_app_credentials, true)
42
+ store_app_credentials(
43
+ app_id: result['id'].to_s,
44
+ private_key: result['pem'],
45
+ client_id: result['client_id'],
46
+ client_secret: result['client_secret'],
47
+ webhook_secret: result['webhook_secret']
48
+ )
49
+ end
50
+
51
+ { result: result }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/github/helpers/client'
4
+ require 'legion/extensions/github/helpers/browser_auth'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Github
9
+ module CLI
10
+ module Auth
11
+ include Helpers::Client
12
+
13
+ def login(client_id: nil, client_secret: nil, scopes: nil, **)
14
+ cid = client_id || settings_client_id
15
+ csec = client_secret || settings_client_secret
16
+ sc = scopes || settings_scopes
17
+
18
+ unless cid && csec
19
+ return { error: 'missing_config',
20
+ description: 'Set github.oauth.client_id or github.app.client_id and github.app.client_secret in settings or pass as arguments' }
21
+ end
22
+
23
+ browser = Helpers::BrowserAuth.new(client_id: cid, client_secret: csec, scopes: sc)
24
+ result = browser.authenticate
25
+
26
+ if result[:result]&.dig('access_token') && respond_to?(:store_oauth_token, true)
27
+ user = begin
28
+ current_user(token: result[:result]['access_token'])
29
+ rescue StandardError => _e
30
+ 'default'
31
+ end
32
+ store_oauth_token(
33
+ user: user,
34
+ access_token: result[:result]['access_token'],
35
+ refresh_token: result[:result]['refresh_token'],
36
+ expires_in: result[:result]['expires_in']
37
+ )
38
+ end
39
+
40
+ result
41
+ end
42
+
43
+ def status(**)
44
+ cred = resolve_credential
45
+ return { result: { authenticated: false } } unless cred
46
+
47
+ user_info = {}
48
+ scopes = nil
49
+
50
+ begin
51
+ response = connection(token: cred[:token]).get('/user')
52
+ user_info = response.body || {}
53
+ headers = response.respond_to?(:headers) ? response.headers : {}
54
+ scopes_header = headers['X-OAuth-Scopes'] || headers['x-oauth-scopes']
55
+ scopes = scopes_header&.split(',')&.map(&:strip)
56
+ rescue StandardError => _e
57
+ user_info = {}
58
+ scopes = nil
59
+ end
60
+
61
+ { result: { authenticated: true, auth_type: cred[:auth_type],
62
+ user: user_info['login'], scopes: scopes } }
63
+ end
64
+
65
+ private
66
+
67
+ def current_user(token:)
68
+ connection(token: token).get('/user').body['login']
69
+ end
70
+
71
+ def settings_client_id
72
+ return nil unless defined?(Legion::Settings)
73
+
74
+ Legion::Settings.dig(:github, :oauth, :client_id) ||
75
+ Legion::Settings.dig(:github, :app, :client_id)
76
+ rescue StandardError => _e
77
+ nil
78
+ end
79
+
80
+ def settings_client_secret
81
+ return nil unless defined?(Legion::Settings)
82
+
83
+ Legion::Settings.dig(:github, :app, :client_secret)
84
+ rescue StandardError => _e
85
+ nil
86
+ end
87
+
88
+ def settings_scopes
89
+ return nil unless defined?(Legion::Settings)
90
+
91
+ Legion::Settings.dig(:github, :oauth, :scopes)
92
+ rescue StandardError => _e
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end