logi-cli 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e169d7a77d8a0d247460b3010da3abcc4925c47a579582d24efcc90839f8466e
4
+ data.tar.gz: 6ad06a8bd52b41c8b5ebb5d12262edd848d4355519ed573bf47a15c22e9278ce
5
+ SHA512:
6
+ metadata.gz: 80c5570bc590c7224a8049aeecd116264de4b916e2e2a42af5ffe9b46f1cfbe089256c121f575a44d0ea99acda20ea00aa23ef3cdbb57e3a4e009d3b662a13b3
7
+ data.tar.gz: 6d252d9bc5884d0da4c195423017e36689227200c312d0dd240cffed2478a87d3c02fbde246be8da849309a2ac0d6aae418384fb95767f19c2474c4be37d58a6
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to **logi-cli** are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-05-27
10
+
11
+ Initial public release on RubyGems.org.
12
+
13
+ ### Added
14
+ - `logi login` — Browser-based OAuth 2.0 + PKCE sign-in, issues and stores a Personal API Key.
15
+ - `logi login --code` — Device-code flow for headless / SSH environments.
16
+ - `logi whoami` — Show current sign-in.
17
+ - `logi logout` — Delete stored credentials.
18
+ - `logi apps {list,show,create,verify,add-redirect,remove-redirect,rotate-secret,delete}` — Full OAuth application management for the signed-in user's organization.
19
+ - `logi token inspect <JWT>` — Decode + verify JWT signatures against the JWKS endpoint.
20
+ - Environment variable overrides: `LOGI_API_URL`, `LOGI_PORTAL_URL`, `LOGI_API_KEY`, `LOGI_CONFIG_PATH`.
21
+
22
+ ### Security
23
+ - Credentials stored at `~/.config/logi/credentials.json` with `chmod 600`.
24
+ - PAK plaintext shown to user once on issue; never re-displayed.
25
+ - Stored values truncated to 18-char prefix in logs.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dcode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.ko.md ADDED
@@ -0,0 +1,102 @@
1
+ # logi-cli
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/logi-cli.svg)](https://rubygems.org/gems/logi-cli)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ English: [README.md](./README.md)
7
+
8
+ logi Identity Provider 를 터미널에서 관리하는 CLI.
9
+
10
+ ## 설치
11
+
12
+ 편한 방법 하나 고르세요:
13
+
14
+ ```bash
15
+ # Homebrew (macOS)
16
+ brew install dcode-co/logi/logi
17
+
18
+ # RubyGems (Ruby ≥ 3.3 환경 어디서나)
19
+ gem install logi-cli
20
+ ```
21
+
22
+ 확인:
23
+
24
+ ```bash
25
+ logi --version # logi-cli 0.1.0
26
+ logi # 환영 + 로그인 상태
27
+ logi login # 브라우저 열림 → 로그인 → PAK 자동 저장
28
+ ```
29
+
30
+ `gem install logi-cli` 후 `which logi` 가 비어있으면 gem `bin` 디렉토리가 PATH 에 없는 것:
31
+
32
+ ```bash
33
+ echo 'export PATH="$(gem environment gemdir)/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
34
+ ```
35
+
36
+ <details>
37
+ <summary>개발 (소스에서)</summary>
38
+
39
+ ```bash
40
+ cd cli && bundle install
41
+ bundle exec bin/logi # 환영 + 다음 단계 안내
42
+ bundle exec bin/logi login # 브라우저 OAuth 로그인
43
+ bundle exec bin/logi apps list # 내 OAuth 앱 보기
44
+ ```
45
+
46
+ 설치 후 `bin` 디렉토리를 `PATH` 에 추가하면 `logi` 만 입력 가능.
47
+
48
+ </details>
49
+
50
+ ## 명령어
51
+
52
+ | 명령 | 설명 |
53
+ |---|---|
54
+ | `logi` | 환영 + 현재 상태(로그인 여부) + 추천 다음 단계 |
55
+ | `logi login` | 브라우저로 리다이렉트 → 권한 승인 → PAK 자동 발급 |
56
+ | `logi whoami` | 로그인된 계정/PAK 정보 |
57
+ | `logi logout` | 저장된 credentials 삭제 |
58
+ | `logi apps list` | OAuth 앱 목록 |
59
+ | `logi apps show <ID>` | 앱 상세 |
60
+ | `logi apps create --name X -r URL` | 새 앱 등록 |
61
+ | `logi apps verify <ID> -r URL` | RP 연동 진단 (tier/redirect_uri/env 권장값) |
62
+ | `logi apps add-redirect <ID> URL` | redirect_uri 추가 |
63
+ | `logi apps remove-redirect <ID> URL` | redirect_uri 제거 |
64
+ | `logi apps rotate-secret <ID>` | client_secret 회전 (owner/admin만) |
65
+ | `logi apps delete <ID>` | 앱 삭제 (owner/admin만) |
66
+ | `logi token inspect <JWT>` | JWT 디코드 + JWKS 서명 검증 |
67
+
68
+ ## 환경변수
69
+
70
+ | 변수 | 설명 |
71
+ |---|---|
72
+ | `LOGI_API_URL` | API 서버 URL (기본 `https://api.1pass.dev`) |
73
+ | `LOGI_PORTAL_URL` | Developer Portal URL (기본 `https://start.1pass.dev`) |
74
+ | `LOGI_API_KEY` | PAK 직접 주입 (CI/CD 친화) |
75
+ | `LOGI_CONFIG_PATH` | credentials 파일 경로 |
76
+
77
+ ## 보안
78
+
79
+ - credentials 파일은 `chmod 600`으로 저장
80
+ - PAK는 출력·로그에 prefix 8자만 노출
81
+ - `logi login`은 OAuth 2.0 PKCE 흐름 — 브라우저에서만 비밀번호 입력
82
+ - `logi logout`은 서버 측 PAK도 revoke (API 호출 별도 추가 예정)
83
+
84
+ ## 미구현
85
+
86
+ `test-flow`, `team`, `audit-log` 등은 v0.2 예정.
87
+
88
+ ## 진단 (verify)
89
+
90
+ RP 측에서 "리다이렉트가 안 돌아온다" 같은 문제가 있을 때 가장 먼저:
91
+
92
+ ```bash
93
+ logi apps verify $CLIENT_ID -r "https://yourapp.com/auth/callback"
94
+ ```
95
+
96
+ 체크 항목:
97
+ - 앱 status (approved/pending)
98
+ - tier (production/sandbox — sandbox면 prod 도메인 차단)
99
+ - redirect_uri 등록 여부
100
+ - RP 권장 환경변수 (`LOGI_API_URL` / `LOGI_CLIENT_ID` / `LOGI_CLIENT_SECRET`)
101
+
102
+ 자세한 트러블슈팅: https://docs.1pass.dev/guide/troubleshooting
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # logi-cli
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/logi-cli.svg)](https://rubygems.org/gems/logi-cli)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Command-line tool for managing the logi Identity Provider from your terminal.
7
+
8
+ 한국어 버전: [README.ko.md](./README.ko.md)
9
+
10
+ ## Install
11
+
12
+ Pick whichever feels native:
13
+
14
+ ```bash
15
+ # Homebrew (macOS)
16
+ brew install dcode-co/logi/logi
17
+
18
+ # RubyGems (anywhere with Ruby ≥ 3.3)
19
+ gem install logi-cli
20
+ ```
21
+
22
+ Verify:
23
+
24
+ ```bash
25
+ logi --version # logi-cli 0.1.0
26
+ logi # welcome + sign-in status
27
+ logi login # opens browser → sign in → PAK auto-saved
28
+ ```
29
+
30
+ If `gem install logi-cli` succeeds but `which logi` is empty on macOS, your gem `bin` dir isn't on `PATH`:
31
+
32
+ ```bash
33
+ echo 'export PATH="$(gem environment gemdir)/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
34
+ ```
35
+
36
+ <details>
37
+ <summary>Development (from source)</summary>
38
+
39
+ ```bash
40
+ cd cli && bundle install
41
+ bundle exec bin/logi # Welcome screen + suggested next steps
42
+ bundle exec bin/logi login # Browser OAuth sign-in
43
+ bundle exec bin/logi apps list # List your OAuth apps
44
+ ```
45
+
46
+ Once installed, add `bin/` to your `PATH` and you can just type `logi`.
47
+
48
+ </details>
49
+
50
+ ## Commands
51
+
52
+ | Command | What it does |
53
+ |---|---|
54
+ | `logi` | Welcome screen + current sign-in status + suggested next steps |
55
+ | `logi login` | Opens your browser → grant access → issues a PAK automatically |
56
+ | `logi whoami` | Show the signed-in account and PAK |
57
+ | `logi logout` | Delete saved credentials |
58
+ | `logi apps list` | List your OAuth apps |
59
+ | `logi apps show <ID>` | Show app details |
60
+ | `logi apps create --name X -r URL` | Register a new app |
61
+ | `logi apps verify <ID> -r URL` | Check an RP integration (tier / redirect URI / env values) |
62
+ | `logi apps add-redirect <ID> URL` | Add a redirect URI |
63
+ | `logi apps remove-redirect <ID> URL` | Remove a redirect URI |
64
+ | `logi apps rotate-secret <ID>` | Rotate `client_secret` (owner/admin only) |
65
+ | `logi apps delete <ID>` | Delete an app (owner/admin only) |
66
+ | `logi token inspect <JWT>` | Decode a JWT and verify its signature against JWKS |
67
+
68
+ ## Environment variables
69
+
70
+ | Variable | What it does |
71
+ |---|---|
72
+ | `LOGI_API_URL` | API server URL (default `https://api.1pass.dev`) |
73
+ | `LOGI_PORTAL_URL` | Developer Portal URL (default `https://start.1pass.dev`) |
74
+ | `LOGI_API_KEY` | Use a PAK directly (handy in CI/CD) |
75
+ | `LOGI_CONFIG_PATH` | Path to the credentials file |
76
+
77
+ ## Security
78
+
79
+ - Credentials are stored with `chmod 600`.
80
+ - Only the first 8 characters of a PAK are ever shown in output or logs.
81
+ - `logi login` uses OAuth 2.0 with PKCE — you only enter your password in the browser.
82
+ - `logi logout` will also revoke the PAK on the server (server-side revocation is coming in a follow-up).
83
+
84
+ ## Not yet implemented
85
+
86
+ `test-flow`, `team`, `audit-log` and a few others are planned for v0.2.
87
+
88
+ ## Diagnosing (`verify`)
89
+
90
+ When someone integrating an RP says "the redirect never comes back," start here:
91
+
92
+ ```bash
93
+ logi apps verify $CLIENT_ID -r "https://yourapp.com/auth/callback"
94
+ ```
95
+
96
+ It checks:
97
+ - App status (approved / pending)
98
+ - Tier (production / sandbox — sandbox apps can't talk to prod domains)
99
+ - Whether the redirect URI is registered
100
+ - Recommended environment variables for the RP (`LOGI_API_URL` / `LOGI_CLIENT_ID` / `LOGI_CLIENT_SECRET`)
101
+
102
+ Full troubleshooting guide: https://docs.1pass.dev/guide/troubleshooting
data/bin/logi ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/logi"
5
+
6
+ Logi::CLI.start(ARGV)
data/lib/logi/cli.rb ADDED
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logi
4
+ class CLI < Thor
5
+ # When invoked with no command, show a friendly welcome instead of Thor's default help dump.
6
+ default_task :welcome
7
+
8
+ desc "welcome", "logi CLI welcome screen (default)", hide: true
9
+ def welcome
10
+ pastel = Pastel.new
11
+ logged_in = Config.load.authenticated?
12
+
13
+ puts pastel.bold("logi") + pastel.dim(" — Identity Provider CLI")
14
+ puts ""
15
+
16
+ if logged_in
17
+ config = Config.load
18
+ puts pastel.green("✓ Signed in") + pastel.dim(" — #{config.api_url}")
19
+ puts ""
20
+ puts pastel.bold("Common commands")
21
+ puts " logi apps list # List your OAuth apps"
22
+ puts " logi apps create --name \"My App\" -r URL # Register a new app"
23
+ puts " logi apps verify <id> -r URL # Check an RP integration (env / redirect URI)"
24
+ puts " logi apps rotate-secret <id> # Rotate client_secret"
25
+ puts " logi whoami # Show sign-in info"
26
+ puts " logi --help # All commands"
27
+ else
28
+ puts pastel.yellow("You're not signed in yet.")
29
+ puts ""
30
+ puts pastel.bold("Get started in 3 steps:")
31
+ puts pastel.dim(" 1.") + " " + pastel.cyan("logi login") + pastel.dim(" # Sign in via your browser")
32
+ puts pastel.dim(" 2.") + " " + pastel.cyan("logi apps create --name \"App name\" -r URL") + pastel.dim(" # Register your first app")
33
+ puts pastel.dim(" 3.") + " " + pastel.cyan("logi apps list") + pastel.dim(" # Check your apps")
34
+ puts ""
35
+ puts pastel.dim("Docs: https://docs.1pass.dev")
36
+ end
37
+ end
38
+
39
+ # Thor convention: --version / -v flag → :version task. Without these maps
40
+ # `logi --version` falls back to default_task (:welcome) which surprises
41
+ # users coming from gh/cargo/npm where `--version` is universal.
42
+ map %w[--version -v] => :version
43
+ desc "version", "Show the logi-cli version"
44
+ def version
45
+ puts "logi-cli #{Logi::VERSION}"
46
+ end
47
+
48
+ desc "login", "Sign in with your logi account and save a PAK"
49
+ method_option :api_url, type: :string, default: nil, desc: "logi API URL (default: https://api.1pass.dev)"
50
+ method_option :portal_url, type: :string, default: nil, desc: "Developer Portal URL (default: https://start.1pass.dev)"
51
+ method_option :name, type: :string, default: "CLI", desc: "PAK name"
52
+ method_option :scope, type: :array, default: nil, desc: "Scopes to request"
53
+ method_option :code, type: :boolean, default: false, desc: "Sign in with a code instead of a browser (for SSH / headless environments)"
54
+ def login
55
+ api_url = options[:api_url] || ENV["LOGI_API_URL"] || Config::DEFAULT_API_URL
56
+ portal_url = options[:portal_url] || ENV["LOGI_PORTAL_URL"]
57
+
58
+ if options[:code]
59
+ Commands::DeviceLogin.perform(api_url: api_url, portal_url: portal_url)
60
+ else
61
+ Commands::Login.perform(
62
+ api_url: api_url,
63
+ portal_url: portal_url,
64
+ name: options[:name],
65
+ scopes: options[:scope]
66
+ )
67
+ end
68
+ end
69
+
70
+ desc "whoami", "Show who owns the current PAK"
71
+ def whoami
72
+ config = Config.load
73
+ pastel = Pastel.new
74
+ unless config.authenticated?
75
+ abort pastel.red("You're not signed in. Run `logi login`.")
76
+ end
77
+
78
+ puts "API URL: #{config.api_url}"
79
+ puts "Source: #{config.source}"
80
+ puts "Name: #{config.name || '-'}"
81
+ puts "PAK: #{config.api_key[0, 18]}…"
82
+ end
83
+
84
+ desc "logout", "Delete saved credentials"
85
+ def logout
86
+ Config.clear
87
+ puts "✓ Signed out (credentials deleted)"
88
+ end
89
+
90
+ desc "apps SUBCOMMAND", "Manage apps (list / show / create / rotate-secret / add-redirect / remove-redirect / delete)"
91
+ subcommand "apps", Commands::App
92
+
93
+ # Backwards-compat singular alias.
94
+ desc "app SUBCOMMAND", "(alias) same as `apps`", hide: true
95
+ subcommand "app", Commands::App
96
+
97
+ desc "token SUBCOMMAND", "JWT access token tools (inspect)"
98
+ subcommand "token", Commands::Token
99
+
100
+ def self.exit_on_failure?
101
+ true
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logi
4
+ module Commands
5
+ class App < Thor
6
+ class_option :json, type: :boolean, default: false,
7
+ desc: "JSON output mode for LLM and CI workflows. You can also set LOGI_OUTPUT=json."
8
+
9
+ desc "list", "List your registered apps"
10
+ def list
11
+ data = authed_client.get("/api/v1/applications")
12
+ apps = data["applications"]
13
+
14
+ Output.success({ count: apps.size, applications: apps }, options) do
15
+ if apps.empty?
16
+ puts pastel.dim("You don't have any apps yet.")
17
+ else
18
+ table = TTY::Table.new(
19
+ header: [ "name", "client_id", "status", "redirect_uris" ],
20
+ rows: apps.map { |a| [ a["name"], a["client_id"], a["status"], a["redirect_uris"].first ] }
21
+ )
22
+ puts table.render(:unicode, padding: [ 0, 1 ])
23
+ end
24
+ end
25
+ rescue HttpClient::Error => e
26
+ Output.failure(code: "list_failed", message: e.body.to_s, status: e.status, options: options) do
27
+ abort pastel.red("Couldn't list apps: #{e.status} #{e.body}")
28
+ end
29
+ end
30
+
31
+ desc "show ID", "Show app details"
32
+ def show(id)
33
+ data = authed_client.get("/api/v1/applications/#{id}")
34
+ Output.success(data, options) do
35
+ puts JSON.pretty_generate(data)
36
+ end
37
+ rescue HttpClient::Error => e
38
+ Output.failure(code: "show_failed", message: e.body.to_s, status: e.status, options: options) do
39
+ abort pastel.red("Couldn't fetch app: #{e.status} #{e.body}")
40
+ end
41
+ end
42
+
43
+ desc "create", "Register a new app"
44
+ method_option :name, type: :string, required: true
45
+ method_option :redirect_uri, type: :string, required: true, aliases: "-r"
46
+ method_option :scope, type: :array, default: [], aliases: "-s",
47
+ desc: "Scopes to enable (e.g. -s openid profile:basic email). Optional."
48
+ method_option :webhook_url, type: :string
49
+ def create
50
+ data = authed_client.post("/api/v1/applications", body: {
51
+ application: {
52
+ name: options[:name],
53
+ redirect_uris: [ options[:redirect_uri] ],
54
+ allowed_scopes: options[:scope],
55
+ webhook_url: options[:webhook_url]
56
+ }.compact
57
+ })
58
+ Output.success(data, options) do
59
+ puts pastel.green("✓ App created")
60
+ puts " client_id: #{data['client_id']}"
61
+ puts " client_secret: #{pastel.yellow(data['client_secret'])}"
62
+ puts pastel.dim(" ⚠ The secret is only shown once. Store it somewhere safe.")
63
+ end
64
+ rescue HttpClient::Error => e
65
+ Output.failure(code: "create_failed", message: e.body.to_s, status: e.status, options: options) do
66
+ abort pastel.red("Couldn't create app: #{e.status} #{e.body}")
67
+ end
68
+ end
69
+
70
+ desc "rotate-secret ID", "Rotate the client_secret"
71
+ def rotate_secret(id)
72
+ data = authed_client.post("/api/v1/applications/#{id}/rotate_secret")
73
+ puts pastel.green("✓ Secret rotated")
74
+ puts " client_secret: #{pastel.yellow(data['client_secret'])}"
75
+ puts pastel.dim(" ⚠ The old secret has been revoked immediately.")
76
+ rescue HttpClient::Error => e
77
+ abort pastel.red("Couldn't rotate secret: #{e.status} #{e.body}")
78
+ end
79
+
80
+ desc "delete ID", "Delete an app"
81
+ def delete(id)
82
+ authed_client.delete("/api/v1/applications/#{id}")
83
+ puts pastel.green("✓ Deleted")
84
+ rescue HttpClient::Error => e
85
+ abort pastel.red("Couldn't delete app: #{e.status} #{e.body}")
86
+ end
87
+
88
+ desc "add-redirect ID URI", "Add a redirect_uri (does nothing if already registered)"
89
+ def add_redirect(id, uri)
90
+ client = authed_client
91
+ current = client.get("/api/v1/applications/#{id}")["redirect_uris"] || []
92
+ if current.include?(uri)
93
+ puts pastel.dim("Already registered: #{uri}")
94
+ return
95
+ end
96
+ client.patch("/api/v1/applications/#{id}", body: {
97
+ application: { redirect_uris: current + [ uri ] }
98
+ })
99
+ puts pastel.green("✓ Added: #{uri}")
100
+ rescue HttpClient::Error => e
101
+ abort pastel.red("Couldn't add redirect: #{e.status} #{e.body}")
102
+ end
103
+
104
+ desc "verify ID", "Check an RP integration: app status, redirect_uri registration, and reachability"
105
+ method_option :redirect_uri, type: :string, aliases: "-r",
106
+ desc: "The RP callback URL. Checks registration and reachability."
107
+ def verify(id)
108
+ client = authed_client
109
+ app = client.get("/api/v1/applications/#{id}")
110
+ target_uri = options[:redirect_uri]
111
+
112
+ if options[:json]
113
+ result = build_verify_result(app, target_uri, client)
114
+ puts JSON.pretty_generate(result)
115
+ exit(result[:checks].all? { |c| c[:ok] } ? 0 : 1)
116
+ end
117
+
118
+ puts pastel.bold("logi verify ") + pastel.dim(id)
119
+ puts ""
120
+
121
+ # Check 1: app status
122
+ if app["status"] == "approved"
123
+ ok "App status: approved"
124
+ else
125
+ warn "App status: #{app['status']} (production traffic needs status=approved)"
126
+ end
127
+
128
+ # Check 2: tier
129
+ tier = app["tier"]
130
+ if tier == "production"
131
+ ok "Tier: production"
132
+ else
133
+ warn "Tier: sandbox — only localhost / staging redirects are allowed. To use a production URL, request a tier upgrade."
134
+ end
135
+
136
+ # Check 3: redirect_uri registration
137
+ registered = app["redirect_uris"] || []
138
+ puts pastel.dim(" Registered redirect_uris:")
139
+ registered.each { |u| puts pastel.dim(" - #{u}") }
140
+
141
+ if target_uri
142
+ if registered.include?(target_uri)
143
+ ok "redirect_uri is registered: #{target_uri}"
144
+ else
145
+ err "redirect_uri is NOT registered: #{target_uri}"
146
+ puts pastel.dim(" → logi apps add-redirect #{id} '#{target_uri}'")
147
+ end
148
+
149
+ # If the app is sandbox-tier but the target URL is on a prod domain, warn.
150
+ if tier == "sandbox" && target_uri =~ %r{https?://(?!localhost|127\.0\.0\.1)([^/]+)}
151
+ host = ::Regexp.last_match(1)
152
+ unless host.match?(/\.staging\.|\.test\.|\.localhost$/)
153
+ err "Sandbox tier but a prod domain (#{host}) — this will be blocked."
154
+ puts pastel.dim(" → Request a tier upgrade, or use a staging domain.")
155
+ end
156
+ end
157
+ end
158
+
159
+ # Check 4: env vars cheatsheet
160
+ puts ""
161
+ puts pastel.bold("Recommended environment variables for the RP:")
162
+ api_url = Config.load.api_url
163
+ puts " LOGI_API_URL=#{api_url}"
164
+ puts " LOGI_CLIENT_ID=#{app['client_id']}"
165
+ puts " LOGI_CLIENT_SECRET=" + pastel.dim("(issued via rotate-secret)")
166
+ puts ""
167
+ puts pastel.dim("Docs: https://docs.1pass.dev/guide/troubleshooting")
168
+ rescue HttpClient::Error => e
169
+ Output.failure(code: "verify_failed", message: e.body.to_s, status: e.status, options: options) do
170
+ abort pastel.red("Couldn't verify app: #{e.status} #{e.body}")
171
+ end
172
+ end
173
+
174
+ desc "remove-redirect ID URI", "Remove a redirect_uri (keeps at least one)"
175
+ def remove_redirect(id, uri)
176
+ client = authed_client
177
+ current = client.get("/api/v1/applications/#{id}")["redirect_uris"] || []
178
+ unless current.include?(uri)
179
+ puts pastel.dim("Not registered: #{uri}")
180
+ return
181
+ end
182
+ if current.size <= 1
183
+ abort pastel.red("At least one redirect_uri is required. Add another URI first.")
184
+ end
185
+ client.patch("/api/v1/applications/#{id}", body: {
186
+ application: { redirect_uris: current - [ uri ] }
187
+ })
188
+ puts pastel.green("✓ Removed: #{uri}")
189
+ rescue HttpClient::Error => e
190
+ abort pastel.red("Couldn't remove redirect: #{e.status} #{e.body}")
191
+ end
192
+
193
+ no_commands do
194
+ def pastel
195
+ @pastel ||= Pastel.new
196
+ end
197
+
198
+ def authed_client
199
+ config = Config.load
200
+ unless config.authenticated?
201
+ abort pastel.red("You need to sign in. Run `logi login`.")
202
+ end
203
+ HttpClient.new(base_url: config.api_url, api_key: config.api_key)
204
+ end
205
+
206
+ def ok(msg); puts pastel.green(" ✓ ") + msg; end
207
+ def warn(msg); puts pastel.yellow(" ⚠ ") + msg; end
208
+ def err(msg); puts pastel.red(" ✗ ") + msg; end
209
+
210
+ def build_verify_result(app, target_uri, _client)
211
+ checks = []
212
+ checks << { name: "status_approved", ok: app["status"] == "approved", value: app["status"] }
213
+ checks << { name: "tier_production", ok: app["tier"] == "production", value: app["tier"] }
214
+ if target_uri
215
+ registered = (app["redirect_uris"] || []).include?(target_uri)
216
+ checks << { name: "redirect_uri_registered", ok: registered, value: target_uri }
217
+ end
218
+ {
219
+ client_id: app["client_id"],
220
+ tier: app["tier"],
221
+ status: app["status"],
222
+ redirect_uris: app["redirect_uris"],
223
+ target_redirect_uri: target_uri,
224
+ checks: checks,
225
+ recommended_env: {
226
+ "LOGI_API_URL" => Config.load.api_url,
227
+ "LOGI_CLIENT_ID" => app["client_id"],
228
+ "LOGI_CLIENT_SECRET" => "(rotate-secret to issue)"
229
+ }
230
+ }
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end