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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.ko.md +102 -0
- data/README.md +102 -0
- data/bin/logi +6 -0
- data/lib/logi/cli.rb +104 -0
- data/lib/logi/commands/app.rb +235 -0
- data/lib/logi/commands/device_login.rb +134 -0
- data/lib/logi/commands/login.rb +182 -0
- data/lib/logi/commands/token.rb +71 -0
- data/lib/logi/config.rb +52 -0
- data/lib/logi/http_client.rb +98 -0
- data/lib/logi/output.rb +58 -0
- data/lib/logi/version.rb +5 -0
- data/lib/logi.rb +23 -0
- metadata +148 -0
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
|
+
[](https://rubygems.org/gems/logi-cli)
|
|
4
|
+
[](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
|
+
[](https://rubygems.org/gems/logi-cli)
|
|
4
|
+
[](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
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
|