unbound-cli 0.8.1 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -29,15 +29,18 @@ class ApiError extends Error {
29
29
  }
30
30
  }
31
31
 
32
- function request(method, path, { body, query, apiKey } = {}) {
33
- const baseUrl = config.getBaseUrl();
32
+ function request(method, path, { body, query, apiKey, baseUrl } = {}) {
33
+ // Explicit baseUrl wins over env-var-aware getBaseUrl(). Used by login /
34
+ // setup / onboard so a user's just-passed --backend-url isn't shadowed by a
35
+ // stale UNBOUND_API_URL env var from a prior shell session.
36
+ const resolvedBaseUrl = baseUrl || config.getBaseUrl();
34
37
  const key = apiKey || config.getApiKey();
35
38
 
36
39
  if (!key) {
37
40
  return Promise.reject(new Error('Not logged in. Run `unbound login` first.'));
38
41
  }
39
42
 
40
- const url = new URL(path, baseUrl);
43
+ const url = new URL(path, resolvedBaseUrl);
41
44
  if (query) {
42
45
  for (const [k, v] of Object.entries(query)) {
43
46
  if (v !== undefined && v !== null) {
@@ -101,10 +104,42 @@ function request(method, path, { body, query, apiKey } = {}) {
101
104
  });
102
105
  }
103
106
 
107
+ // Plain unauthenticated GET to an arbitrary URL. Returns the raw response body
108
+ // as a UTF-8 string. Callers that want JSON must do JSON.parse() themselves.
109
+ // Used by `unbound oacb` to fetch baseline JSONs and hook scripts from GitHub.
110
+ function getRaw(url) {
111
+ return new Promise((resolve, reject) => {
112
+ const parsed = new URL(url);
113
+ const transport = parsed.protocol === 'https:' ? https : http;
114
+ const req = transport.get(url, { headers: { 'User-Agent': USER_AGENT } }, (res) => {
115
+ const chunks = [];
116
+ res.on('data', (chunk) => chunks.push(chunk));
117
+ res.on('end', () => {
118
+ if (res.statusCode >= 200 && res.statusCode < 300) {
119
+ resolve(Buffer.concat(chunks).toString('utf8'));
120
+ } else {
121
+ reject(new ApiError(res.statusCode, { error: `HTTP ${res.statusCode} fetching ${url}` }));
122
+ }
123
+ });
124
+ res.on('error', (err) => {
125
+ reject(new Error(`Network error fetching ${parsed.host}: ${err.message}`));
126
+ });
127
+ });
128
+ req.setTimeout(30000, () => {
129
+ req.destroy();
130
+ reject(new Error(`Request timed out fetching ${parsed.host}`));
131
+ });
132
+ req.on('error', (err) => {
133
+ reject(new Error(`Network error fetching ${parsed.host}: ${err.message}`));
134
+ });
135
+ });
136
+ }
137
+
104
138
  module.exports = {
105
139
  ApiError,
106
140
  get: (path, opts) => request('GET', path, opts),
107
141
  post: (path, opts) => request('POST', path, opts),
108
142
  put: (path, opts) => request('PUT', path, opts),
109
143
  del: (path, opts) => request('DELETE', path, opts),
144
+ getRaw,
110
145
  };
package/src/auth.js CHANGED
@@ -107,13 +107,17 @@ async function loginWithBrowser(frontendUrl) {
107
107
  * Shows a spinner during validation and a success message with user info.
108
108
  *
109
109
  * @param {string} apiKey - The API key to validate and store
110
+ * @param {object} [opts]
111
+ * @param {string} [opts.baseUrl] - Explicit backend URL override. Used when the
112
+ * caller just persisted a tenant URL via setUrls and wants validation to hit
113
+ * that exact backend, regardless of any stale UNBOUND_API_URL env var.
110
114
  * @returns {Promise<true>}
111
115
  */
112
- async function loginWithApiKey(apiKey) {
116
+ async function loginWithApiKey(apiKey, { baseUrl } = {}) {
113
117
  const spin = output.spinner('Authenticating...');
114
118
  let response;
115
119
  try {
116
- response = await api.get('/api/v1/users/privileges/', { apiKey });
120
+ response = await api.get('/api/v1/users/privileges/', { apiKey, baseUrl });
117
121
  } catch (err) {
118
122
  let msg;
119
123
  if (err.statusCode === 401 || err.statusCode === 403) {
@@ -143,10 +147,14 @@ async function loginWithApiKey(apiKey) {
143
147
  * Ensures the user is logged in. If an apiKey is provided, validates and
144
148
  * stores it first. If not logged in, triggers browser-based login.
145
149
  * Returns true if logged in (or just logged in), false on failure.
150
+ *
151
+ * `baseUrl` and `frontendUrl` are explicit overrides for the URLs to validate
152
+ * against / open in the browser. Used by login/setup/onboard so a just-passed
153
+ * --backend-url / --frontend-url isn't shadowed by a stale env var.
146
154
  */
147
- async function ensureLoggedIn({ apiKey } = {}) {
155
+ async function ensureLoggedIn({ apiKey, baseUrl, frontendUrl } = {}) {
148
156
  if (apiKey) {
149
- await loginWithApiKey(apiKey);
157
+ await loginWithApiKey(apiKey, { baseUrl });
150
158
  return true;
151
159
  }
152
160
 
@@ -155,8 +163,7 @@ async function ensureLoggedIn({ apiKey } = {}) {
155
163
  }
156
164
 
157
165
  output.warn('Not logged in. Opening browser to authenticate...');
158
- const frontendUrl = config.getFrontendUrl();
159
- const result = await loginWithBrowser(frontendUrl);
166
+ const result = await loginWithBrowser(frontendUrl || config.getFrontendUrl());
160
167
 
161
168
  const parts = [];
162
169
  if (result.email) parts.push(`as ${result.email}`);
@@ -47,17 +47,21 @@ Examples:
47
47
  // --backend-url is the canonical visible flag; --base-url is a hidden
48
48
  // back-compat alias. setUrls writes all provided URLs atomically so
49
49
  // a malformed --frontend-url can't leave a fresh --backend-url half-applied.
50
- config.setUrls({
50
+ const written = config.setUrls({
51
51
  backend: opts.backendUrl || opts.baseUrl,
52
52
  frontend: opts.frontendUrl,
53
53
  gateway: opts.gatewayUrl,
54
54
  });
55
-
56
- let apiKey;
55
+ // Prefer just-persisted values over env-var-aware getters so a stale
56
+ // UNBOUND_*_URL from a prior shell session can't shadow the user's
57
+ // explicit --backend-url / --frontend-url and route validation at the
58
+ // wrong tenant.
59
+ const explicitBaseUrl = written.base_url;
60
+ const explicitFrontendUrl = written.frontend_url;
57
61
 
58
62
  if (opts.apiKey) {
59
63
  try {
60
- await loginWithApiKey(opts.apiKey);
64
+ await loginWithApiKey(opts.apiKey, { baseUrl: explicitBaseUrl });
61
65
  } catch {
62
66
  process.exitCode = 1;
63
67
  return;
@@ -69,12 +73,13 @@ Examples:
69
73
  if (opts.domain) {
70
74
  frontendUrl = opts.domain.startsWith('http') ? opts.domain : `https://${opts.domain}`;
71
75
  } else {
72
- frontendUrl = config.getFrontendUrl();
76
+ frontendUrl = explicitFrontendUrl || config.getFrontendUrl();
73
77
  }
74
78
  await loginWithBrowser(frontendUrl);
75
- apiKey = config.getApiKey();
76
- // Validate the stored key
77
- await api.get('/api/v1/users/privileges/');
79
+ // Validate the just-stored key against the explicit backend if one
80
+ // was just persisted; otherwise fall back to the env-var-aware
81
+ // getter. api.get reads the stored key from config internally.
82
+ await api.get('/api/v1/users/privileges/', { baseUrl: explicitBaseUrl });
78
83
  }
79
84
 
80
85
  const cfg = config.readConfig();