unbound-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.
- package/.github/workflows/publish.yml +27 -0
- package/.github/workflows/version-check.yml +35 -0
- package/LOCAL_DEV.md +126 -0
- package/README.md +128 -0
- package/package.json +27 -0
- package/src/api.js +81 -0
- package/src/auth.js +115 -0
- package/src/commands/login.js +63 -0
- package/src/commands/logout.js +28 -0
- package/src/commands/policy.js +412 -0
- package/src/commands/setup.js +341 -0
- package/src/commands/status.js +59 -0
- package/src/commands/tools.js +176 -0
- package/src/commands/user-groups.js +282 -0
- package/src/commands/users.js +88 -0
- package/src/commands/whoami.js +49 -0
- package/src/config.js +87 -0
- package/src/index.js +129 -0
- package/src/output.js +65 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Setup Node.js
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: '20'
|
|
19
|
+
registry-url: 'https://registry.npmjs.org'
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Publish to NPM
|
|
25
|
+
run: npm publish --access public
|
|
26
|
+
env:
|
|
27
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Version Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check-version:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout PR branch
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Get PR version
|
|
16
|
+
id: pr_version
|
|
17
|
+
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
18
|
+
|
|
19
|
+
- name: Checkout main branch
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
ref: main
|
|
23
|
+
path: main-branch
|
|
24
|
+
|
|
25
|
+
- name: Get main version
|
|
26
|
+
id: main_version
|
|
27
|
+
run: echo "version=$(node -p "require('./main-branch/package.json').version")" >> $GITHUB_OUTPUT
|
|
28
|
+
|
|
29
|
+
- name: Compare versions
|
|
30
|
+
run: |
|
|
31
|
+
if [ "${{ steps.pr_version.outputs.version }}" = "${{ steps.main_version.outputs.version }}" ]; then
|
|
32
|
+
echo "::error::Version in package.json (${{ steps.pr_version.outputs.version }}) is the same as main. Please bump the version before merging."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
echo "Version check passed: main=${{ steps.main_version.outputs.version }} → PR=${{ steps.pr_version.outputs.version }}"
|
package/LOCAL_DEV.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Local Development Guide
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Node.js >= 18
|
|
6
|
+
- npm
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd unbound-cli
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Run locally (without installing globally)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Run directly
|
|
19
|
+
node src/index.js --help
|
|
20
|
+
node src/index.js status
|
|
21
|
+
node src/index.js policy list
|
|
22
|
+
|
|
23
|
+
# Or use npm link to get the `unbound` command locally
|
|
24
|
+
npm link
|
|
25
|
+
unbound --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Point to local backend
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Option 1: Set in config (persists across commands)
|
|
32
|
+
node src/index.js config set-url http://localhost:8000
|
|
33
|
+
node src/index.js config set-frontend-url http://localhost:3000
|
|
34
|
+
|
|
35
|
+
# Option 2: Environment variable (per-command)
|
|
36
|
+
UNBOUND_API_URL=http://localhost:8000 node src/index.js policy list
|
|
37
|
+
|
|
38
|
+
# Option 3: Set during login
|
|
39
|
+
node src/index.js login --base-url http://localhost:8000 --frontend-url http://localhost:3000
|
|
40
|
+
|
|
41
|
+
# Reset to production
|
|
42
|
+
node src/index.js config reset-url
|
|
43
|
+
node src/index.js config reset-frontend-url
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Verify config
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node src/index.js config show
|
|
50
|
+
node src/index.js config get-url
|
|
51
|
+
node src/index.js config get-frontend-url
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Test login flow
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Browser-based login (starts local HTTP server, opens browser)
|
|
58
|
+
node src/index.js login --base-url http://localhost:8000 --frontend-url http://localhost:3000
|
|
59
|
+
|
|
60
|
+
# Direct API key login (skip browser)
|
|
61
|
+
node src/index.js login --base-url http://localhost:8000 --api-key <your-key>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Test commands
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Auth
|
|
68
|
+
node src/index.js whoami
|
|
69
|
+
node src/index.js status
|
|
70
|
+
|
|
71
|
+
# Policies
|
|
72
|
+
node src/index.js policy list
|
|
73
|
+
node src/index.js policy list --type SECURITY --json
|
|
74
|
+
node src/index.js policy get 1
|
|
75
|
+
node src/index.js policy form-data
|
|
76
|
+
|
|
77
|
+
# Users
|
|
78
|
+
node src/index.js users list
|
|
79
|
+
node src/index.js users effective-policies 1
|
|
80
|
+
|
|
81
|
+
# Groups
|
|
82
|
+
node src/index.js user-groups list
|
|
83
|
+
node src/index.js groups list # alias
|
|
84
|
+
|
|
85
|
+
# Tools
|
|
86
|
+
node src/index.js tools list
|
|
87
|
+
node src/index.js tools connect CURSOR
|
|
88
|
+
node src/index.js tools approved
|
|
89
|
+
|
|
90
|
+
# Setup
|
|
91
|
+
node src/index.js setup cursor
|
|
92
|
+
node src/index.js setup claude-code
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Unlink when done
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm unlink -g unbound-cli
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Switching environments
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Use local backend and frontend
|
|
105
|
+
unbound config set-url http://localhost:8000
|
|
106
|
+
unbound config set-frontend-url http://localhost:3000
|
|
107
|
+
|
|
108
|
+
# Reset to production
|
|
109
|
+
unbound config reset-url
|
|
110
|
+
unbound config reset-frontend-url
|
|
111
|
+
|
|
112
|
+
# One-time override via env vars
|
|
113
|
+
UNBOUND_API_URL=http://localhost:8000 UNBOUND_FRONTEND_URL=http://localhost:3000 unbound login
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Configuration
|
|
117
|
+
|
|
118
|
+
| Command | Description |
|
|
119
|
+
|---------|-------------|
|
|
120
|
+
| `unbound config set-url <url>` | Set API base URL |
|
|
121
|
+
| `unbound config get-url` | Show current API base URL |
|
|
122
|
+
| `unbound config reset-url` | Reset to default URL |
|
|
123
|
+
| `unbound config set-frontend-url <url>` | Set frontend URL |
|
|
124
|
+
| `unbound config get-frontend-url` | Show current frontend URL |
|
|
125
|
+
| `unbound config reset-frontend-url` | Reset to default frontend URL |
|
|
126
|
+
| `unbound config show` | Show all config values |
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Unbound CLI
|
|
2
|
+
|
|
3
|
+
Command-line tool for managing your Unbound AI Gateway. Configure policies, manage users and groups, connect AI coding tools, and set up tools like Cursor, Claude Code, Gemini CLI, and more.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g unbound-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Login via browser
|
|
15
|
+
unbound login
|
|
16
|
+
|
|
17
|
+
# Or login with an API key
|
|
18
|
+
unbound login --api-key <your-api-key>
|
|
19
|
+
|
|
20
|
+
# Check who you are
|
|
21
|
+
unbound whoami
|
|
22
|
+
|
|
23
|
+
# List policies
|
|
24
|
+
unbound policy list
|
|
25
|
+
|
|
26
|
+
# Set up Cursor to use Unbound
|
|
27
|
+
unbound setup cursor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### Authentication
|
|
33
|
+
|
|
34
|
+
| Command | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| `unbound login` | Sign in via browser or `--api-key` |
|
|
37
|
+
| `unbound logout` | Remove stored credentials |
|
|
38
|
+
| `unbound whoami` | Show current user, org, and role |
|
|
39
|
+
| `unbound status` | Show CLI status and API connectivity |
|
|
40
|
+
|
|
41
|
+
### Policies (Admin only)
|
|
42
|
+
|
|
43
|
+
| Command | Description |
|
|
44
|
+
|---------|-------------|
|
|
45
|
+
| `unbound policy list` | List all policies |
|
|
46
|
+
| `unbound policy get <id>` | Get policy details |
|
|
47
|
+
| `unbound policy create --name <n> --type <t>` | Create a policy |
|
|
48
|
+
| `unbound policy update <id>` | Update a policy |
|
|
49
|
+
| `unbound policy delete <id>` | Delete a policy |
|
|
50
|
+
| `unbound policy form-data` | Get reference data for policy creation |
|
|
51
|
+
| `unbound policy effective <id>` | View effective policies for a user/group |
|
|
52
|
+
|
|
53
|
+
Policy types: `SECURITY`, `MODEL`, `COST`
|
|
54
|
+
|
|
55
|
+
### Users
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
|---------|-------------|
|
|
59
|
+
| `unbound users list` | List organization members |
|
|
60
|
+
| `unbound users effective-policies <id>` | View effective policies for a user |
|
|
61
|
+
|
|
62
|
+
### User Groups (Admin only)
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---------|-------------|
|
|
66
|
+
| `unbound user-groups list` | List all groups |
|
|
67
|
+
| `unbound user-groups get <id>` | Get group details |
|
|
68
|
+
| `unbound user-groups create --name <n>` | Create a group |
|
|
69
|
+
| `unbound user-groups update <id>` | Update a group |
|
|
70
|
+
| `unbound user-groups delete <id>` | Delete a group |
|
|
71
|
+
| `unbound user-groups effective-policies <id>` | View effective policies |
|
|
72
|
+
|
|
73
|
+
Alias: `unbound groups` works the same as `unbound user-groups`.
|
|
74
|
+
|
|
75
|
+
### Tools
|
|
76
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
|---------|-------------|
|
|
79
|
+
| `unbound tools list` | List connected tools |
|
|
80
|
+
| `unbound tools connect <type>` | Connect a tool |
|
|
81
|
+
| `unbound tools approved` | List approved tool types |
|
|
82
|
+
|
|
83
|
+
Supported tool types: `CLAUDE_CODE`, `CURSOR`, `COPILOT`, `ROO_CODE`, `CLINE`, `GEMINI_CLI`, `CODEX`, `KILO_CODE`, `CUSTOM_ACCESS`
|
|
84
|
+
|
|
85
|
+
### Setup
|
|
86
|
+
|
|
87
|
+
Automated setup (downloads scripts, sets env vars, configures tool):
|
|
88
|
+
|
|
89
|
+
| Command | Description |
|
|
90
|
+
|---------|-------------|
|
|
91
|
+
| `unbound setup cursor` | Download hooks, set env, restart Cursor |
|
|
92
|
+
| `unbound setup claude-code` | Configure gateway + install hooks |
|
|
93
|
+
| `unbound setup gemini-cli` | Set GEMINI_API_KEY and base URL |
|
|
94
|
+
| `unbound setup codex` | Set OPENAI_API_KEY and base URL |
|
|
95
|
+
|
|
96
|
+
Instruction-only (shows API key and base URL to configure manually):
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---------|-------------|
|
|
100
|
+
| `unbound setup roo-code` | Show Roo Code config values |
|
|
101
|
+
| `unbound setup cline` | Show Cline config values |
|
|
102
|
+
| `unbound setup kilo-code` | Show Kilo Code config values |
|
|
103
|
+
| `unbound setup custom-access` | Show API key and base URL for direct API access |
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
Config is stored in `~/.unbound/config.json`.
|
|
109
|
+
|
|
110
|
+
**Backend URL priority** (highest to lowest):
|
|
111
|
+
1. `UNBOUND_API_URL` environment variable
|
|
112
|
+
2. `base_url` in `~/.unbound/config.json` (set via `unbound config set-url`)
|
|
113
|
+
3. Default: `https://backend.getunbound.ai`
|
|
114
|
+
|
|
115
|
+
**Frontend URL priority** (highest to lowest):
|
|
116
|
+
1. `UNBOUND_FRONTEND_URL` environment variable
|
|
117
|
+
2. `frontend_url` in `~/.unbound/config.json` (set via `unbound config set-frontend-url`)
|
|
118
|
+
3. Default: `https://gateway.getunbound.ai`
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Global Options
|
|
122
|
+
|
|
123
|
+
All list/get commands support `--json` for machine-readable JSON output.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
unbound policy list --json
|
|
127
|
+
unbound users list --json | jq '.members[].email'
|
|
128
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "unbound-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for Unbound - AI Gateway management",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"unbound": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js",
|
|
11
|
+
"lint": "eslint src/",
|
|
12
|
+
"test": "node --test test/"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"unbound",
|
|
16
|
+
"ai-gateway",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.1.0",
|
|
22
|
+
"open": "^10.1.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { URL } = require('url');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
|
|
6
|
+
const USER_AGENT = 'UnboundCLI/0.1.0';
|
|
7
|
+
|
|
8
|
+
class ApiError extends Error {
|
|
9
|
+
constructor(statusCode, body) {
|
|
10
|
+
const message = body?.error || body?.message || `HTTP ${statusCode}`;
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ApiError';
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.body = body;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function request(method, path, { body, query, apiKey } = {}) {
|
|
19
|
+
const baseUrl = config.getBaseUrl();
|
|
20
|
+
const key = apiKey || config.getApiKey();
|
|
21
|
+
|
|
22
|
+
if (!key) {
|
|
23
|
+
return Promise.reject(new Error('Not logged in. Run `unbound login` first.'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const url = new URL(path, baseUrl);
|
|
27
|
+
if (query) {
|
|
28
|
+
for (const [k, v] of Object.entries(query)) {
|
|
29
|
+
if (v !== undefined && v !== null) {
|
|
30
|
+
url.searchParams.set(k, v);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const headers = {
|
|
36
|
+
'Authorization': `Bearer ${key}`,
|
|
37
|
+
'User-Agent': USER_AGENT,
|
|
38
|
+
'Accept': 'application/json',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (body) {
|
|
42
|
+
headers['Content-Type'] = 'application/json';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
46
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const req = transport.request(url, { method, headers }, (res) => {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
const raw = Buffer.concat(chunks).toString();
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
parsed = raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
62
|
+
resolve(parsed);
|
|
63
|
+
} else {
|
|
64
|
+
reject(new ApiError(res.statusCode, parsed));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
req.on('error', reject);
|
|
70
|
+
if (payload) req.write(payload);
|
|
71
|
+
req.end();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
ApiError,
|
|
77
|
+
get: (path, opts) => request('GET', path, opts),
|
|
78
|
+
post: (path, opts) => request('POST', path, opts),
|
|
79
|
+
put: (path, opts) => request('PUT', path, opts),
|
|
80
|
+
del: (path, opts) => request('DELETE', path, opts),
|
|
81
|
+
};
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared browser-based authentication flow.
|
|
3
|
+
* Used by both `login` and `setup` commands.
|
|
4
|
+
*/
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { URL } = require('url');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
const output = require('./output');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Opens the browser for authentication and waits for the callback.
|
|
12
|
+
* Starts a local HTTP server, opens the frontend auth page, and waits
|
|
13
|
+
* for the redirect with API key, email, and org.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} frontendUrl - The frontend URL to open
|
|
16
|
+
* @returns {Promise<{email: string, orgName: string}>}
|
|
17
|
+
*/
|
|
18
|
+
async function loginWithBrowser(frontendUrl) {
|
|
19
|
+
const server = http.createServer();
|
|
20
|
+
let callbackResolve;
|
|
21
|
+
let callbackReject;
|
|
22
|
+
|
|
23
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
24
|
+
callbackResolve = resolve;
|
|
25
|
+
callbackReject = reject;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
server.on('request', (req, res) => {
|
|
29
|
+
const url = new URL(req.url, `http://localhost`);
|
|
30
|
+
|
|
31
|
+
if (url.pathname !== '/callback') {
|
|
32
|
+
res.writeHead(404);
|
|
33
|
+
res.end('Not found');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const apiKey = url.searchParams.get('api_key');
|
|
38
|
+
const email = url.searchParams.get('email');
|
|
39
|
+
const orgName = url.searchParams.get('org');
|
|
40
|
+
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
43
|
+
res.end('<html><body><h2>Authentication failed</h2><p>No API key received. Please try again.</p></body></html>');
|
|
44
|
+
callbackReject(new Error('No API key received in callback'));
|
|
45
|
+
server.close();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
config.setApiKey(apiKey);
|
|
50
|
+
const cfg = config.readConfig();
|
|
51
|
+
if (email) cfg.email = email;
|
|
52
|
+
if (orgName) cfg.org_name = orgName;
|
|
53
|
+
config.writeConfig(cfg);
|
|
54
|
+
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
56
|
+
res.end('<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
|
|
57
|
+
|
|
58
|
+
callbackResolve({ email, orgName });
|
|
59
|
+
server.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
64
|
+
server.on('error', reject);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const port = server.address().port;
|
|
68
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
69
|
+
const authUrl = `${frontendUrl}/cli/auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
70
|
+
|
|
71
|
+
const open = (await import('open')).default;
|
|
72
|
+
|
|
73
|
+
console.log('Opening browser for authentication...');
|
|
74
|
+
console.log(`If the browser does not open, visit:\n${authUrl}\n`);
|
|
75
|
+
console.log('Waiting for authentication...');
|
|
76
|
+
|
|
77
|
+
await open(authUrl);
|
|
78
|
+
|
|
79
|
+
const timeout = setTimeout(() => {
|
|
80
|
+
callbackReject(new Error('Authentication timed out after 120 seconds'));
|
|
81
|
+
server.close();
|
|
82
|
+
}, 120_000);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await callbackPromise;
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
return result;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Ensures the user is logged in. If not, triggers browser-based login.
|
|
96
|
+
* Returns true if logged in (or just logged in), false on failure.
|
|
97
|
+
*/
|
|
98
|
+
async function ensureLoggedIn() {
|
|
99
|
+
if (config.isLoggedIn()) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
output.warn('Not logged in. Opening browser to authenticate...\n');
|
|
104
|
+
const frontendUrl = config.getFrontendUrl();
|
|
105
|
+
const result = await loginWithBrowser(frontendUrl);
|
|
106
|
+
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (result.email) parts.push(`as ${result.email}`);
|
|
109
|
+
if (result.orgName) parts.push(`to ${result.orgName}`);
|
|
110
|
+
output.success(`Logged in successfully${parts.length ? ' ' + parts.join(' ') : ''}.\n`);
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { loginWithBrowser, ensureLoggedIn };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const output = require('../output');
|
|
3
|
+
const { loginWithBrowser } = require('../auth');
|
|
4
|
+
|
|
5
|
+
function register(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('login')
|
|
8
|
+
.description('Authenticate with Unbound. Opens a browser for interactive login, or use --api-key for CI/CD environments.')
|
|
9
|
+
.option('--api-key <key>', 'Authenticate with an API key directly (non-interactive)')
|
|
10
|
+
.option('--base-url <url>', 'Set a custom API base URL before logging in')
|
|
11
|
+
.option('--frontend-url <url>', 'Set a custom frontend URL for browser login')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Authentication methods:
|
|
14
|
+
Browser login (default):
|
|
15
|
+
Opens a browser to the Unbound frontend for interactive login.
|
|
16
|
+
A local HTTP server listens for the auth callback with the API key.
|
|
17
|
+
Times out after 120 seconds if no callback is received.
|
|
18
|
+
|
|
19
|
+
API key login (--api-key):
|
|
20
|
+
Non-interactive login for CI/CD or automation. Stores the provided
|
|
21
|
+
API key directly without browser interaction.
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--base-url sets the backend API URL before authenticating (persisted).
|
|
25
|
+
--frontend-url sets the frontend URL for browser login (persisted).
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
$ unbound login # Interactive browser-based login
|
|
29
|
+
$ unbound login --api-key sk-abc123 # Non-interactive login with API key
|
|
30
|
+
$ unbound login --base-url http://localhost:8000 --frontend-url http://localhost:3000
|
|
31
|
+
$ unbound login --base-url https://custom.api.example.com --api-key sk-abc123
|
|
32
|
+
`)
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
try {
|
|
35
|
+
if (opts.baseUrl) {
|
|
36
|
+
config.setBaseUrl(opts.baseUrl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (opts.frontendUrl) {
|
|
40
|
+
config.setFrontendUrl(opts.frontendUrl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (opts.apiKey) {
|
|
44
|
+
config.setApiKey(opts.apiKey);
|
|
45
|
+
output.success('Logged in successfully with API key.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const frontendUrl = config.getFrontendUrl();
|
|
50
|
+
const result = await loginWithBrowser(frontendUrl);
|
|
51
|
+
|
|
52
|
+
const parts = [];
|
|
53
|
+
if (result.email) parts.push(`as ${result.email}`);
|
|
54
|
+
if (result.orgName) parts.push(`to ${result.orgName}`);
|
|
55
|
+
output.success(`Logged in successfully${parts.length ? ' ' + parts.join(' ') : ''}.`);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
output.error(err.message);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { register };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const output = require('../output');
|
|
3
|
+
|
|
4
|
+
function register(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('logout')
|
|
7
|
+
.description('Log out of Unbound. Removes stored credentials and configuration from ~/.unbound/config.json.')
|
|
8
|
+
.addHelpText('after', `
|
|
9
|
+
What this does:
|
|
10
|
+
Clears all stored credentials (API key, email, organization) from
|
|
11
|
+
~/.unbound/config.json. Custom URL settings (base_url, frontend_url)
|
|
12
|
+
are also removed.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
$ unbound logout
|
|
16
|
+
`)
|
|
17
|
+
.action(() => {
|
|
18
|
+
try {
|
|
19
|
+
config.clearConfig();
|
|
20
|
+
output.success('Logged out successfully.');
|
|
21
|
+
} catch (err) {
|
|
22
|
+
output.error(err.message);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { register };
|