unbound-cli 0.8.1 → 0.8.2

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.8.2",
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) {
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();
@@ -52,21 +52,29 @@ Examples:
52
52
  let setupSucceeded = false;
53
53
  let discoveryDomain;
54
54
  try {
55
- // Persist explicitly-passed URL flags BEFORE login so tenant URLs land
56
- // in config before any backend call (whoami, setup completion ping) uses
57
- // them. setUrls is atomic a malformed URL throws before any disk write,
58
- // so the three URLs never end up out of sync.
59
- config.setUrls({
55
+ // Persist URLs first, then login, then setup order matters so the
56
+ // login validates against the new backend and setup wires tools at the
57
+ // new gateway. setUrls is atomic; a malformed URL throws before any
58
+ // disk write so the three URLs never end up out of sync.
59
+ const written = config.setUrls({
60
60
  backend: opts.backendUrl,
61
61
  frontend: opts.frontendUrl,
62
62
  gateway: opts.gatewayUrl,
63
63
  });
64
- const backendUrl = config.getBaseUrl();
65
- const frontendUrl = config.getFrontendUrl();
66
- const gatewayUrl = config.getGatewayUrl();
64
+ // Prefer the values we JUST persisted over the env-var-aware getters —
65
+ // a stale UNBOUND_*_URL from a prior shell session could otherwise
66
+ // silently shadow the user's explicit --*-url flag and route login or
67
+ // setup at the wrong tenant.
68
+ const backendUrl = written.base_url || config.getBaseUrl();
69
+ const frontendUrl = written.frontend_url || config.getFrontendUrl();
70
+ const gatewayUrl = written.gateway_url || config.getGatewayUrl();
67
71
  discoveryDomain = opts.domain || backendUrl;
68
72
 
69
- await ensureLoggedIn({ apiKey: opts.apiKey });
73
+ await ensureLoggedIn({
74
+ apiKey: opts.apiKey,
75
+ baseUrl: written.base_url,
76
+ frontendUrl: written.frontend_url,
77
+ });
70
78
  const apiKey = config.getApiKey();
71
79
 
72
80
  console.log('');
@@ -124,15 +132,16 @@ Examples:
124
132
  let setupSucceeded = false;
125
133
  let discoveryDomain;
126
134
  try {
127
- // Persist explicitly-passed URL flags so subsequent commands on this
128
- // device (e.g. unbound discover, unbound setup) hit the same tenant.
129
- // setUrls is atomic a malformed URL throws before any disk write.
130
- config.setUrls({
135
+ // Persist URLs first, then login, then setup order matters so this
136
+ // MDM run wires tools at the new tenant. Prefer just-written values
137
+ // over env-var-aware getters so a stale UNBOUND_*_URL can't shadow the
138
+ // user's explicit --*-url flag.
139
+ const written = config.setUrls({
131
140
  backend: opts.backendUrl,
132
141
  gateway: opts.gatewayUrl,
133
142
  });
134
- const backendUrl = config.getBaseUrl();
135
- const gatewayUrl = config.getGatewayUrl();
143
+ const backendUrl = written.base_url || config.getBaseUrl();
144
+ const gatewayUrl = written.gateway_url || config.getGatewayUrl();
136
145
  discoveryDomain = opts.domain || backendUrl;
137
146
 
138
147
  checkRoot('onboard-mdm');
@@ -367,21 +367,26 @@ automatically to authenticate before proceeding.
367
367
  `)
368
368
  .action(async (tools, opts) => {
369
369
  try {
370
- // Persist explicitly-passed URL flags BEFORE ensureLoggedIn runs so a
371
- // tenant-onboarding `unbound setup --gateway-url ... --api-key ...` lands
372
- // tenant URLs in config before any backend call uses them. setUrls is
373
- // atomic — no partial writes if any value is malformed.
374
- config.setUrls({
370
+ // Persist URLs first, login, then setup. setUrls is atomic; a malformed
371
+ // URL throws before any disk write.
372
+ const written = config.setUrls({
375
373
  backend: opts.backendUrl,
376
374
  frontend: opts.frontendUrl,
377
375
  gateway: opts.gatewayUrl,
378
376
  });
379
-
380
- await ensureLoggedIn({ apiKey: opts.apiKey });
377
+ // Prefer just-persisted values over env-var-aware getters so a stale
378
+ // UNBOUND_*_URL from a prior shell session can't silently shadow the
379
+ // user's explicit --*-url flag and route login/setup at the wrong tenant.
380
+ const backendUrl = written.base_url || config.getBaseUrl();
381
+ const frontendUrl = written.frontend_url || config.getFrontendUrl();
382
+ const gatewayUrl = written.gateway_url || config.getGatewayUrl();
383
+
384
+ await ensureLoggedIn({
385
+ apiKey: opts.apiKey,
386
+ baseUrl: written.base_url,
387
+ frontendUrl: written.frontend_url,
388
+ });
381
389
  const apiKey = config.getApiKey();
382
- const backendUrl = config.getBaseUrl();
383
- const frontendUrl = config.getFrontendUrl();
384
- const gatewayUrl = config.getGatewayUrl();
385
390
  const urlOpts = { backendUrl, frontendUrl, gatewayUrl };
386
391
 
387
392
  // --all expands to the default bundle. Cannot be combined with explicit tool names.
@@ -593,15 +598,16 @@ Examples:
593
598
  // --backend-url, --frontend-url, --gateway-url are defined only on the parent `setup` command.
594
599
  // Use optsWithGlobals() so they all work regardless of position relative to `mdm`.
595
600
  const globalOpts = command.optsWithGlobals();
596
- // Persist explicitly-passed URL flags so this MDM run also configures
597
- // the CLI for any subsequent non-MDM commands on the same machine.
598
- // setUrls is atomic no partial writes if any value is malformed.
599
- config.setUrls({
601
+ // Persist URLs first so this MDM run wires tools at the new tenant
602
+ // and any subsequent non-MDM command on the same machine inherits.
603
+ // Prefer just-persisted values over env-var-aware getters so a stale
604
+ // UNBOUND_*_URL can't shadow the explicit --*-url flag.
605
+ const written = config.setUrls({
600
606
  backend: globalOpts.backendUrl,
601
607
  gateway: globalOpts.gatewayUrl,
602
608
  });
603
- const backendUrl = config.getBaseUrl();
604
- const gatewayUrl = config.getGatewayUrl();
609
+ const backendUrl = written.base_url || config.getBaseUrl();
610
+ const gatewayUrl = written.gateway_url || config.getGatewayUrl();
605
611
 
606
612
  if (globalOpts.all && tools.length > 0) {
607
613
  output.error('Cannot combine --all with specific tool names. Use one or the other.');
@@ -151,6 +151,49 @@ test('setUrls is atomic — partial failure leaves config unchanged', () => {
151
151
  }
152
152
  });
153
153
 
154
+ // WEB-4107: setUrls returns the just-written values so callers can use them
155
+ // directly instead of going back through getBaseUrl/etc. (which give env-var
156
+ // precedence over config). A stale UNBOUND_*_URL from a prior shell session
157
+ // would otherwise silently shadow the user's explicit --*-url flag.
158
+ test('setUrls return value reflects what was persisted, bypassing env-var shadowing', () => {
159
+ const fs = require('node:fs');
160
+ const os = require('node:os');
161
+ const path = require('node:path');
162
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-cli-test-'));
163
+ const origHome = process.env.HOME;
164
+ const origGw = process.env.UNBOUND_GATEWAY_URL;
165
+ const origBe = process.env.UNBOUND_API_URL;
166
+ process.env.HOME = tmp;
167
+ // Stale env vars from a prior tenant — these would otherwise win in getXxxUrl().
168
+ process.env.UNBOUND_GATEWAY_URL = 'https://gw.STALE.com';
169
+ process.env.UNBOUND_API_URL = 'https://be.STALE.com';
170
+ delete require.cache[require.resolve('../src/config')];
171
+ const c = require('../src/config');
172
+ try {
173
+ const written = c.setUrls({
174
+ backend: 'be.NEW.com',
175
+ frontend: 'fe.NEW.com',
176
+ gateway: 'gw.NEW.com',
177
+ });
178
+ // The return value reflects WHAT WAS WRITTEN — the user's explicit input.
179
+ assert.equal(written.base_url, 'https://be.new.com');
180
+ assert.equal(written.frontend_url, 'https://fe.new.com');
181
+ assert.equal(written.gateway_url, 'https://gw.new.com');
182
+ // The getters would still respect the env vars (existing convention).
183
+ // Callers that need the user's intent must use the setUrls return value.
184
+ assert.equal(c.getGatewayUrl(), 'https://gw.STALE.com');
185
+ assert.equal(c.getBaseUrl(), 'https://be.STALE.com');
186
+ } finally {
187
+ process.env.HOME = origHome;
188
+ if (origGw === undefined) delete process.env.UNBOUND_GATEWAY_URL;
189
+ else process.env.UNBOUND_GATEWAY_URL = origGw;
190
+ if (origBe === undefined) delete process.env.UNBOUND_API_URL;
191
+ else process.env.UNBOUND_API_URL = origBe;
192
+ delete require.cache[require.resolve('../src/config')];
193
+ fs.rmSync(tmp, { recursive: true, force: true });
194
+ }
195
+ });
196
+
154
197
  // WEB-4103: backfillUserInfo must refresh email/org_name when the API returns
155
198
  // a different value (e.g. user switched tenants), not just fill if missing.
156
199
  // Defensive: must NOT blank the cached value on a partial/empty API response.