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 +1 -1
- package/src/api.js +6 -3
- package/src/auth.js +13 -6
- package/src/commands/login.js +13 -8
- package/src/commands/onboard.js +24 -15
- package/src/commands/setup.js +22 -16
- package/test/config-normalize-url.test.js +43 -0
package/package.json
CHANGED
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
|
-
|
|
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,
|
|
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
|
|
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}`);
|
package/src/commands/login.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
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();
|
package/src/commands/onboard.js
CHANGED
|
@@ -52,21 +52,29 @@ Examples:
|
|
|
52
52
|
let setupSucceeded = false;
|
|
53
53
|
let discoveryDomain;
|
|
54
54
|
try {
|
|
55
|
-
// Persist
|
|
56
|
-
//
|
|
57
|
-
//
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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({
|
|
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
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
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');
|
package/src/commands/setup.js
CHANGED
|
@@ -367,21 +367,26 @@ automatically to authenticate before proceeding.
|
|
|
367
367
|
`)
|
|
368
368
|
.action(async (tools, opts) => {
|
|
369
369
|
try {
|
|
370
|
-
// Persist
|
|
371
|
-
//
|
|
372
|
-
|
|
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
|
-
|
|
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
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
|
|
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.
|