@senticor/hive 1.42.1 → 1.43.1

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/hive.cjs +566 -198
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,6 +49,7 @@ hive login alice
49
49
 
50
50
  # Remote shell (SSH / Coder / container) where the browser is on another host:
51
51
  # prints the URL — authenticate anywhere, then paste the code / callback URL back.
52
+ # An imperfect paste gets up to three attempts; the printed link stays valid.
52
53
  hive login --no-browser
53
54
 
54
55
  # Verify the session, then go.
package/hive.cjs CHANGED
@@ -7771,6 +7771,27 @@ var import_node_child_process2 = require("child_process");
7771
7771
  var import_node_url3 = require("url");
7772
7772
  var import_node_path3 = require("path");
7773
7773
 
7774
+ // ../hive-api-contracts/src/admin-orgs.ts
7775
+ init_cjs_shims();
7776
+ function isValidCorsOrigin(value) {
7777
+ let url2;
7778
+ try {
7779
+ url2 = new URL(value);
7780
+ } catch {
7781
+ return false;
7782
+ }
7783
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") return false;
7784
+ if (url2.username || url2.password) return false;
7785
+ if (url2.host === "") return false;
7786
+ if (url2.pathname !== "/" && url2.pathname !== "") return false;
7787
+ if (url2.search || url2.hash) return false;
7788
+ return url2.origin === value;
7789
+ }
7790
+ var ROLE_KEY_RE = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
7791
+ function hasTenantAudienceDelimiter(displayKey) {
7792
+ return /[\s,]/.test(String(displayKey || "").trim());
7793
+ }
7794
+
7774
7795
  // src/lib/client.ts
7775
7796
  init_cjs_shims();
7776
7797
  var import_node_http2 = __toESM(require("http"), 1);
@@ -8079,6 +8100,159 @@ function callbackPage(msg, kind) {
8079
8100
  </body>
8080
8101
  </html>`;
8081
8102
  }
8103
+ var MAX_PASTE_ATTEMPTS = 3;
8104
+ function providerDetail(status2, data) {
8105
+ const error51 = typeof data.error === "string" ? data.error.trim() : "";
8106
+ const described = typeof data.error_description === "string" ? data.error_description.trim() : "";
8107
+ if (described) return error51 ? `${described} (${error51})` : described;
8108
+ if (error51) {
8109
+ const flat = error51.replace(/\s+/g, " ").trim();
8110
+ return flat.length > 200 ? `${flat.slice(0, 200)}\u2026` : flat;
8111
+ }
8112
+ return `HTTP ${status2 ?? "?"}`;
8113
+ }
8114
+ function classifyTokenFailure(res) {
8115
+ const error51 = typeof res.data.error === "string" ? res.data.error.trim() : "";
8116
+ const detail = providerDetail(res.status, res.data);
8117
+ if (error51 === "invalid_grant") {
8118
+ const fromAnotherRun = /pkce|redirect_uri/i.test(
8119
+ typeof res.data.error_description === "string" ? res.data.error_description : ""
8120
+ );
8121
+ return {
8122
+ retryable: true,
8123
+ headline: "The authorization server rejected that code",
8124
+ detail,
8125
+ hint: fromAnotherRun ? "That code belongs to a different 'hive login' run \u2014 use the link printed above." : "Codes are single-use and expire within about a minute, and a truncated paste looks the same. Open this link again for a fresh code:",
8126
+ needsFreshCode: true
8127
+ };
8128
+ }
8129
+ if (error51 === "invalid_client" || error51 === "unauthorized_client" || error51 === "invalid_request") {
8130
+ return {
8131
+ retryable: false,
8132
+ headline: "The authorization server refused the token request",
8133
+ detail,
8134
+ hint: "Check the OIDC client and issuer this CLI is pointed at; pasting another code cannot fix this.",
8135
+ needsFreshCode: false
8136
+ };
8137
+ }
8138
+ return {
8139
+ retryable: false,
8140
+ headline: "The authorization server could not complete the token request",
8141
+ detail,
8142
+ hint: "This looks like a server-side problem \u2014 try again in a moment.",
8143
+ needsFreshCode: false
8144
+ };
8145
+ }
8146
+ function pasteFailure(e) {
8147
+ const fromAnotherRun = /state mismatch/i.test(e.message);
8148
+ return {
8149
+ retryable: true,
8150
+ headline: "Could not read that paste",
8151
+ detail: e.message,
8152
+ hint: fromAnotherRun ? "That URL is from a different 'hive login' run \u2014 use the link printed above." : "Copy the whole address from the browser address bar, or just the code=\u2026 value.",
8153
+ needsFreshCode: fromAnotherRun
8154
+ };
8155
+ }
8156
+ function networkFailure(e, endpoint) {
8157
+ return {
8158
+ retryable: false,
8159
+ headline: `Could not reach the authorization server at ${endpoint}`,
8160
+ detail: e.message,
8161
+ hint: "Check the issuer URL and your network; pasting another code cannot fix this.",
8162
+ needsFreshCode: false
8163
+ };
8164
+ }
8165
+ function missingTokenFailure(status2) {
8166
+ return {
8167
+ retryable: false,
8168
+ headline: "The authorization server returned no access token",
8169
+ detail: `HTTP ${status2 ?? "?"} without an access_token field`,
8170
+ hint: "This looks like a server-side problem; pasting another code cannot fix it.",
8171
+ needsFreshCode: false
8172
+ };
8173
+ }
8174
+ function formatRetryNotice(f, attemptsLeft, authUrl) {
8175
+ const left = `${attemptsLeft} attempt${attemptsLeft === 1 ? "" : "s"} left.`;
8176
+ const link = f.needsFreshCode ? `
8177
+ ${authUrl}` : "";
8178
+ return `
8179
+ ${f.headline}: ${f.detail}
8180
+ ${f.hint}${link}
8181
+ ${left}
8182
+ `;
8183
+ }
8184
+ function formatFinalError(f, attempts, opts) {
8185
+ const command = opts.noBrowser ? "hive login --no-browser" : "hive login";
8186
+ const middle = opts.stopped ? opts.stopped === "eof" ? "No further input available (stdin closed)." : "No further input available (stdin is not a terminal)." : attempts > 1 ? `No attempts left (${attempts} of ${attempts}).` : "";
8187
+ const tail2 = opts.stopped ? `Re-run '${command}' from a terminal, or set HIVE_TOKEN for non-interactive use.` : f.retryable ? `Re-run '${command}' for a fresh link.` : f.hint;
8188
+ return [`${f.headline} \u2014 ${f.detail}.`, middle, tail2].filter(Boolean).join(" ");
8189
+ }
8190
+ function createPastePrompter(input = process.stdin, output2 = process.stdout) {
8191
+ const rl = import_node_readline.default.createInterface({ input, output: output2 });
8192
+ let closed = false;
8193
+ let inFlight = null;
8194
+ let deliver = null;
8195
+ rl.on("close", () => {
8196
+ closed = true;
8197
+ const pending2 = deliver;
8198
+ deliver = null;
8199
+ inFlight = null;
8200
+ pending2?.({ kind: "eof" });
8201
+ });
8202
+ return {
8203
+ ask(query) {
8204
+ if (closed) return Promise.resolve({ kind: "eof" });
8205
+ if (inFlight) return inFlight;
8206
+ inFlight = new Promise((resolve3) => {
8207
+ deliver = resolve3;
8208
+ rl.question(query, (answer) => {
8209
+ deliver = null;
8210
+ inFlight = null;
8211
+ resolve3({ kind: "paste", text: answer.trim() });
8212
+ });
8213
+ });
8214
+ return inFlight;
8215
+ },
8216
+ canReask: () => !closed && input.isTTY === true,
8217
+ close: () => {
8218
+ try {
8219
+ rl.close();
8220
+ } catch {
8221
+ }
8222
+ }
8223
+ };
8224
+ }
8225
+ function createPeekable(produce) {
8226
+ let pending2 = null;
8227
+ return {
8228
+ peek: () => pending2 ??= produce(),
8229
+ /** The value `peek()` resolved with was used — arm a fresh slot. */
8230
+ consume: () => {
8231
+ pending2 = null;
8232
+ }
8233
+ };
8234
+ }
8235
+ function createInbox() {
8236
+ const queued = [];
8237
+ let waiter = null;
8238
+ return {
8239
+ push(value) {
8240
+ if (waiter) {
8241
+ const w = waiter;
8242
+ waiter = null;
8243
+ w(value);
8244
+ } else {
8245
+ queued.push(value);
8246
+ }
8247
+ },
8248
+ take() {
8249
+ const buffered = queued.shift();
8250
+ return buffered !== void 0 ? Promise.resolve(buffered) : new Promise((resolve3) => {
8251
+ waiter = resolve3;
8252
+ });
8253
+ }
8254
+ };
8255
+ }
8082
8256
  async function loginWithBrowser(issuerUrl, opts = {}) {
8083
8257
  const issuer = issuerUrl || process.env.HIVE_OIDC_ISSUER_URL || DEFAULT_ISSUER;
8084
8258
  const disco = await getDiscovery(issuer);
@@ -8091,122 +8265,171 @@ async function loginWithBrowser(issuerUrl, opts = {}) {
8091
8265
  const open = opts.open || openBrowser;
8092
8266
  const clientId = opts.clientId || process.env.HIVE_OIDC_CLIENT_ID || CLIENT_ID;
8093
8267
  const timeoutMs = opts.timeoutMs ?? 3e5;
8094
- const { code, redirectUri } = await new Promise((resolve3, reject) => {
8095
- let redirectUri2 = "";
8096
- let timer;
8097
- let settled = false;
8098
- let rl;
8099
- const cleanup = () => {
8100
- clearTimeout(timer);
8101
- try {
8102
- server.close();
8103
- } catch {
8104
- }
8105
- try {
8106
- rl?.close();
8107
- } catch {
8108
- }
8109
- };
8110
- const succeed = (code2) => {
8111
- if (settled) return;
8112
- settled = true;
8113
- cleanup();
8114
- resolve3({ code: code2, redirectUri: redirectUri2 });
8115
- };
8116
- const abort = (e) => {
8117
- if (settled) return;
8118
- settled = true;
8119
- cleanup();
8120
- reject(e);
8268
+ const tokenEndpoint = rewriteForInternalKeycloak(disco.token_endpoint, issuer);
8269
+ const maxAttempts = opts.noBrowser ? Math.max(1, opts.maxPasteAttempts ?? MAX_PASTE_ATTEMPTS) : 1;
8270
+ const inbox = createInbox();
8271
+ let prompter;
8272
+ let timer;
8273
+ const server = import_node_http.default.createServer((req2, res) => {
8274
+ const u = new import_node_url.URL(req2.url || "/", "http://127.0.0.1");
8275
+ if (u.pathname !== "/callback") {
8276
+ res.statusCode = 404;
8277
+ res.setHeader("content-type", "text/plain; charset=utf-8");
8278
+ res.end("Not found");
8279
+ return;
8280
+ }
8281
+ const fail = (m, e) => {
8282
+ res.statusCode = 400;
8283
+ res.setHeader("content-type", "text/html; charset=utf-8");
8284
+ res.end(callbackPage(m, "error"));
8285
+ inbox.push({ kind: "fatal", error: e });
8121
8286
  };
8122
- const server = import_node_http.default.createServer((req2, res2) => {
8123
- const u = new import_node_url.URL(req2.url || "/", "http://127.0.0.1");
8124
- if (u.pathname !== "/callback") {
8125
- res2.statusCode = 404;
8126
- res2.setHeader("content-type", "text/plain; charset=utf-8");
8127
- res2.end("Not found");
8128
- return;
8129
- }
8130
- const fail = (m, e) => {
8131
- res2.statusCode = 400;
8132
- res2.setHeader("content-type", "text/html; charset=utf-8");
8133
- res2.end(callbackPage(m, "error"));
8134
- abort(e);
8135
- };
8136
- const err = u.searchParams.get("error");
8137
- if (err) return fail("Sign-in cancelled.", new Error(`Authorization failed: ${err}`));
8138
- if (u.searchParams.get("state") !== state) return fail("Invalid state.", new Error("OAuth state mismatch"));
8139
- const gotCode = u.searchParams.get("code");
8140
- if (!gotCode) return fail("No code received.", new Error("No authorization code in callback"));
8141
- res2.statusCode = 200;
8142
- res2.setHeader("content-type", "text/html; charset=utf-8");
8143
- res2.end(callbackPage("Signed in successfully.", "success"));
8144
- succeed(gotCode);
8287
+ const err = u.searchParams.get("error");
8288
+ if (err) return fail("Sign-in cancelled.", new Error(`Authorization failed: ${err}`));
8289
+ if (u.searchParams.get("state") !== state) return fail("Invalid state.", new Error("OAuth state mismatch"));
8290
+ const gotCode = u.searchParams.get("code");
8291
+ if (!gotCode) return fail("No code received.", new Error("No authorization code in callback"));
8292
+ res.statusCode = 200;
8293
+ res.setHeader("content-type", "text/html; charset=utf-8");
8294
+ res.end(callbackPage("Signed in successfully.", "success"));
8295
+ inbox.push({ kind: "loopback", code: gotCode });
8296
+ });
8297
+ try {
8298
+ const redirectUri = await new Promise((resolve3, reject) => {
8299
+ const onListenError = (e) => reject(e);
8300
+ server.once("error", onListenError);
8301
+ server.listen(0, "127.0.0.1", () => {
8302
+ server.removeListener("error", onListenError);
8303
+ server.on("error", (e) => inbox.push({ kind: "fatal", error: e }));
8304
+ resolve3(`http://127.0.0.1:${server.address().port}/callback`);
8305
+ });
8145
8306
  });
8146
- server.on("error", (e) => abort(e));
8147
- server.listen(0, "127.0.0.1", () => {
8148
- const port = server.address().port;
8149
- redirectUri2 = `http://127.0.0.1:${port}/callback`;
8150
- const authUrl = new import_node_url.URL(disco.authorization_endpoint);
8151
- authUrl.searchParams.set("response_type", "code");
8152
- authUrl.searchParams.set("client_id", clientId);
8153
- authUrl.searchParams.set("redirect_uri", redirectUri2);
8154
- authUrl.searchParams.set("scope", "openid profile email");
8155
- authUrl.searchParams.set("state", state);
8156
- authUrl.searchParams.set("code_challenge", challenge);
8157
- authUrl.searchParams.set("code_challenge_method", "S256");
8158
- if (opts.loginHint) authUrl.searchParams.set("login_hint", opts.loginHint);
8159
- const href = authUrl.toString();
8160
- if (opts.noBrowser) {
8161
- console.log("\nOpen this link in a browser on any device and sign in:");
8162
- console.log(` ${href}
8307
+ const authUrl = new import_node_url.URL(disco.authorization_endpoint);
8308
+ authUrl.searchParams.set("response_type", "code");
8309
+ authUrl.searchParams.set("client_id", clientId);
8310
+ authUrl.searchParams.set("redirect_uri", redirectUri);
8311
+ authUrl.searchParams.set("scope", "openid profile email");
8312
+ authUrl.searchParams.set("state", state);
8313
+ authUrl.searchParams.set("code_challenge", challenge);
8314
+ authUrl.searchParams.set("code_challenge_method", "S256");
8315
+ if (opts.loginHint) authUrl.searchParams.set("login_hint", opts.loginHint);
8316
+ const href = authUrl.toString();
8317
+ if (opts.noBrowser) {
8318
+ console.log("\nOpen this link in a browser on any device and sign in:");
8319
+ console.log(` ${href}
8163
8320
  `);
8164
- console.log(
8165
- "You are then redirected to a 127.0.0.1 page, which may not load here.\nCopy the address from the address bar (or just the code=\u2026 value) and paste it below.\n"
8321
+ console.log(
8322
+ "You are then redirected to a 127.0.0.1 page, which may not load here.\nCopy the address from the address bar (or just the code=\u2026 value) and paste it below.\n"
8323
+ );
8324
+ } else {
8325
+ console.log("\nOpening the browser to sign in (or copy the link):");
8326
+ console.log(` ${href}
8327
+ `);
8328
+ open(href);
8329
+ }
8330
+ const deadline = new Promise((resolve3) => {
8331
+ timer = setTimeout(
8332
+ () => resolve3({ kind: "fatal", error: new Error("Login timed out \u2014 not confirmed in time.") }),
8333
+ timeoutMs
8334
+ );
8335
+ });
8336
+ let attempts = 0;
8337
+ const pasteQuery = () => maxAttempts > 1 && attempts > 0 ? `Paste the code or callback URL (attempt ${attempts + 1} of ${maxAttempts}): ` : "Paste the code or callback URL: ";
8338
+ const loopback = createPeekable(() => inbox.take());
8339
+ let paste;
8340
+ let canReask = () => true;
8341
+ if (opts.noBrowser) {
8342
+ const injected = opts.prompt;
8343
+ if (injected) {
8344
+ paste = createPeekable(
8345
+ () => injected(pasteQuery()).then(
8346
+ (text) => text === null || text === void 0 ? { kind: "eof" } : { kind: "paste", text },
8347
+ (e) => ({ kind: "fatal", error: e })
8348
+ )
8166
8349
  );
8167
- const onPaste = (pasted) => {
8168
- try {
8169
- succeed(parsePastedCallback(pasted, state));
8170
- } catch (e) {
8171
- abort(e);
8172
- }
8173
- };
8174
- if (opts.prompt) {
8175
- opts.prompt("Paste the code or callback URL: ").then(onPaste).catch((e) => abort(e));
8176
- } else {
8177
- rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
8178
- rl.question("Paste the code or callback URL: ", onPaste);
8179
- }
8180
8350
  } else {
8181
- console.log("\nOpening the browser to sign in (or copy the link):");
8182
- console.log(` ${href}
8183
- `);
8184
- open(href);
8351
+ const live = createPastePrompter();
8352
+ prompter = live;
8353
+ paste = createPeekable(() => live.ask(pasteQuery()));
8354
+ canReask = () => live.canReask();
8355
+ }
8356
+ }
8357
+ let last;
8358
+ const nextStep = (f) => {
8359
+ if (!f.retryable || attempts >= maxAttempts) return "exhausted";
8360
+ if (!paste || !canReask()) return maxAttempts > 1 ? "no-input" : "exhausted";
8361
+ console.error(formatRetryNotice(f, maxAttempts - attempts, href));
8362
+ return "retry";
8363
+ };
8364
+ const giveUp = (f, step) => new Error(
8365
+ formatFinalError(f, attempts, {
8366
+ noBrowser: Boolean(opts.noBrowser),
8367
+ stopped: step === "no-input" ? "not-a-tty" : void 0
8368
+ })
8369
+ );
8370
+ for (; ; ) {
8371
+ const sources = paste ? [loopback.peek(), paste.peek(), deadline] : [loopback.peek(), deadline];
8372
+ const ev = await Promise.race(sources);
8373
+ if (ev.kind === "loopback") loopback.consume();
8374
+ if (ev.kind === "paste" || ev.kind === "eof") paste?.consume();
8375
+ if (ev.kind === "fatal") throw ev.error;
8376
+ if (ev.kind === "eof") {
8377
+ throw last ? new Error(formatFinalError(last, attempts, { noBrowser: true, stopped: "eof" })) : new Error(
8378
+ "No code was entered \u2014 stdin closed before any input. Run 'hive login --no-browser' from a terminal, or set HIVE_TOKEN for non-interactive use."
8379
+ );
8185
8380
  }
8186
- timer = setTimeout(() => abort(new Error("Login timed out \u2014 not confirmed in time.")), timeoutMs);
8187
- });
8188
- });
8189
- const tokenEndpoint = rewriteForInternalKeycloak(disco.token_endpoint, issuer);
8190
- const res = await fetchUrlencoded(tokenEndpoint, {
8191
- grant_type: "authorization_code",
8192
- client_id: clientId,
8193
- code,
8194
- redirect_uri: redirectUri,
8195
- code_verifier: verifier
8196
- });
8197
- if (!res.ok) {
8198
- throw new Error(res.data.error_description || `Token exchange failed with status ${res.status}`);
8381
+ attempts += 1;
8382
+ let code;
8383
+ if (ev.kind === "loopback") {
8384
+ code = ev.code;
8385
+ } else {
8386
+ try {
8387
+ code = parsePastedCallback(ev.text, state);
8388
+ } catch (e) {
8389
+ last = pasteFailure(e);
8390
+ const step2 = nextStep(last);
8391
+ if (step2 === "retry") continue;
8392
+ throw giveUp(last, step2);
8393
+ }
8394
+ }
8395
+ let res;
8396
+ try {
8397
+ res = await fetchUrlencoded(tokenEndpoint, {
8398
+ grant_type: "authorization_code",
8399
+ client_id: clientId,
8400
+ code,
8401
+ redirect_uri: redirectUri,
8402
+ code_verifier: verifier
8403
+ });
8404
+ } catch (e) {
8405
+ throw giveUp(networkFailure(e, tokenEndpoint), "exhausted");
8406
+ }
8407
+ const accessToken = typeof res.data.access_token === "string" ? res.data.access_token : "";
8408
+ if (res.ok && accessToken) {
8409
+ const claims2 = parseJwt(accessToken);
8410
+ saveCredentials({
8411
+ issuer,
8412
+ access_token: accessToken,
8413
+ refresh_token: res.data.refresh_token,
8414
+ expires_at: Date.now() + (res.data.expires_in || 3600) * 1e3,
8415
+ user: claims2 ? claims2.preferred_username || claims2.sub : opts.loginHint,
8416
+ clientId
8417
+ });
8418
+ return true;
8419
+ }
8420
+ last = res.ok ? missingTokenFailure(res.status) : classifyTokenFailure(res);
8421
+ const step = nextStep(last);
8422
+ if (step === "retry") continue;
8423
+ throw giveUp(last, step);
8424
+ }
8425
+ } finally {
8426
+ clearTimeout(timer);
8427
+ try {
8428
+ server.close();
8429
+ } catch {
8430
+ }
8431
+ prompter?.close();
8199
8432
  }
8200
- const claims2 = parseJwt(res.data.access_token);
8201
- saveCredentials({
8202
- issuer,
8203
- access_token: res.data.access_token,
8204
- refresh_token: res.data.refresh_token,
8205
- expires_at: Date.now() + (res.data.expires_in || 3600) * 1e3,
8206
- user: claims2 ? claims2.preferred_username || claims2.sub : opts.loginHint,
8207
- clientId
8208
- });
8209
- return true;
8210
8433
  }
8211
8434
  async function refreshToken(creds) {
8212
8435
  if (!creds || !creds.refresh_token) return false;
@@ -8543,16 +8766,36 @@ async function fetchAuthConfig(apiUrl) {
8543
8766
 
8544
8767
  // src/lib/errors.ts
8545
8768
  init_cjs_shims();
8769
+
8770
+ // src/lib/tenant-selection.ts
8771
+ init_cjs_shims();
8772
+ var applied = null;
8773
+ function recordTenantSelection(selection) {
8774
+ applied = selection;
8775
+ }
8776
+ function appliedTenantSelection() {
8777
+ return applied;
8778
+ }
8779
+ function describeAppliedTenant(selection) {
8780
+ if (!selection.applied || selection.value === null) {
8781
+ return "No X-Hive-Tenant was sent, so the server resolved your self-tenant";
8782
+ }
8783
+ return `Sent X-Hive-Tenant: ${selection.value} (${selection.source}, from ${selection.origin})`;
8784
+ }
8785
+
8786
+ // src/lib/errors.ts
8546
8787
  var CliCommandError = class extends Error {
8547
8788
  exitCode;
8548
8789
  symbolicCode;
8549
8790
  remediation;
8791
+ tenant;
8550
8792
  constructor(symbolicCode, exitCode, message, options) {
8551
8793
  super(message, options);
8552
8794
  this.name = "CliCommandError";
8553
8795
  this.exitCode = exitCode;
8554
8796
  this.symbolicCode = symbolicCode;
8555
8797
  this.remediation = options?.remediation;
8798
+ this.tenant = options?.tenant;
8556
8799
  }
8557
8800
  };
8558
8801
  function commandError(symbolicCode, exitCode, message, options) {
@@ -8593,14 +8836,36 @@ function schemaErrorFields(data) {
8593
8836
  }
8594
8837
  function remoteFailureRemediation(data) {
8595
8838
  const code = remoteErrorFields(data).code.toUpperCase();
8839
+ const scope = tenantFailureScope();
8596
8840
  if (code === "TENANT_NOT_ALLOWED") {
8597
- return "The requested tenant is not in your active memberships. For platform-admin operations (org/mission create) omit --tenant / HIVE_TENANT. If the tenant is saved in CLI config, run 'hive config clear --tenant'; then run 'hive diag authz' to list memberships and optionally pin one with 'hive config use --tenant=<displayKey>'.";
8841
+ const selection = appliedTenantSelection();
8842
+ const pinned = selection?.applied === true && selection.source === "pinned";
8843
+ return scope + "The requested tenant is not in your active memberships. " + (pinned ? "It is a saved pin, not something this command asked for \u2014 run 'hive config clear --tenant' to drop it. " : "If the tenant is saved in CLI config, run 'hive config clear --tenant'. ") + "Platform-scoped commands (hive org *, hive admin list|grant|revoke) ignore a pin already; for other platform-admin operations (mission create) omit --tenant / HIVE_TENANT. Run 'hive diag authz' to list memberships and optionally pin one with 'hive config use --tenant=<displayKey>'.";
8598
8844
  }
8599
8845
  if (code === "MISSION_NOT_FOUND") {
8600
- return "The mission id was not found in the CURRENT tenant scope. A mission is only visible under the tenant it was created in \u2014 check you are using the same --tenant / HIVE_TENANT (or self-tenant) as at create time. Run 'hive mission list --tenant <tenant>' to find the scope that owns it.";
8846
+ return scope + "The mission id was not found in the CURRENT tenant scope. A mission is only visible under the tenant it was created in \u2014 check you are using the same --tenant / HIVE_TENANT (or self-tenant) as at create time. Run 'hive mission list --tenant <tenant>' to find the scope that owns it.";
8601
8847
  }
8602
8848
  return void 0;
8603
8849
  }
8850
+ function tenantFailureScope() {
8851
+ const selection = appliedTenantSelection();
8852
+ return selection ? `${describeAppliedTenant(selection)}. ` : "";
8853
+ }
8854
+ function remoteFailureTenant(data) {
8855
+ const code = remoteErrorFields(data).code.toUpperCase();
8856
+ if (code !== "TENANT_NOT_ALLOWED" && code !== "MISSION_NOT_FOUND") return void 0;
8857
+ return appliedTenantSelection() ?? void 0;
8858
+ }
8859
+ function remoteFailureError(status2, data, action, exitCode = exitCodeForStatus(status2)) {
8860
+ const remediation = remoteFailureRemediation(data);
8861
+ const tenant = remoteFailureTenant(data);
8862
+ return commandError(
8863
+ "REMOTE_FAILED",
8864
+ exitCode,
8865
+ `${action} failed${status2 ? ` (HTTP ${status2})` : ""}: ${remoteFailureText(data)}${remediation ? ` \u2014 ${remediation}` : ""}`,
8866
+ remediation || tenant ? { ...remediation ? { remediation } : {}, ...tenant ? { tenant } : {} } : void 0
8867
+ );
8868
+ }
8604
8869
  function remoteFailureText(data) {
8605
8870
  const body = data;
8606
8871
  const { code, message } = remoteErrorFields(data);
@@ -8618,7 +8883,8 @@ function toStructuredError(err) {
8618
8883
  error: {
8619
8884
  code,
8620
8885
  message,
8621
- ...e?.remediation ? { remediation: e.remediation } : {}
8886
+ ...e?.remediation ? { remediation: e.remediation } : {},
8887
+ ...e?.tenant ? { tenant: e.tenant } : {}
8622
8888
  }
8623
8889
  };
8624
8890
  }
@@ -8667,9 +8933,13 @@ function installRemoteFailureExit() {
8667
8933
  if (!failure) return;
8668
8934
  const symbolic = remoteErrorFields(failure.body).code || remoteFailureCode(failure.status);
8669
8935
  const remediation = remoteFailureRemediation(failure.body) ?? (failure.status === 401 ? "Run 'hive login' to authenticate, or set HIVE_TOKEN." : void 0);
8936
+ const tenant = remoteFailureTenant(failure.body);
8670
8937
  const message = `${failure.method} ${failure.path} failed${failure.status ? ` (HTTP ${failure.status})` : ""}: ${remoteFailureText(failure.body)}`;
8671
8938
  if (isJsonMode()) {
8672
- console.error(JSON.stringify({ ok: false, error: { code: symbolic, message, ...remediation ? { remediation } : {} } }));
8939
+ console.error(JSON.stringify({
8940
+ ok: false,
8941
+ error: { code: symbolic, message, ...remediation ? { remediation } : {}, ...tenant ? { tenant } : {} }
8942
+ }));
8673
8943
  } else {
8674
8944
  console.error(`\x1B[31mError:\x1B[0m ${symbolic}: ${message}`);
8675
8945
  if (remediation) console.error(remediation);
@@ -8758,15 +9028,40 @@ function normalizeArgv(argv) {
8758
9028
  }
8759
9029
  return [...positionals, ...flags];
8760
9030
  }
8761
- function ctx(args) {
9031
+ function ctx(args, opts = {}) {
8762
9032
  const cfg = readConfig();
8763
- const resolvedTenant = resolveTenantContext(args, cfg);
8764
- const tenant = resolvesToSelfTenant(resolvedTenant) ? null : resolvedTenant.value;
9033
+ const resolvedTenant = rawTenantContext(args, cfg);
9034
+ const pinIgnored = opts.adminScope === true && resolvedTenant.source === "pinned";
9035
+ const selfTenant = resolvesToSelfTenant(resolvedTenant);
9036
+ let problem = null;
9037
+ if (!pinIgnored && !selfTenant) {
9038
+ try {
9039
+ assertCanonicalTenantSelector(resolvedTenant.value, resolvedTenant.source);
9040
+ } catch (err) {
9041
+ if (!opts.diagnostic) throw err;
9042
+ problem = err instanceof Error ? err.message : String(err);
9043
+ }
9044
+ }
9045
+ const tenant = pinIgnored || selfTenant || problem !== null ? null : resolvedTenant.value;
8765
9046
  if (tenant === null && resolvedTenant.source === "pinned" && resolvedTenant.value === "default" && shouldEmitHuman(args)) {
8766
- process.stderr.write(
8767
- "Note: ignoring a saved tenant 'default' (a legacy login fallback that matches no tenant). Using your self-tenant; run 'hive config clear --tenant' to remove it, or pass --tenant <id>.\n"
9047
+ noteOnce(
9048
+ "Note: ignoring a saved tenant 'default' (a legacy login fallback that matches no tenant). Using your self-tenant; run 'hive config clear --tenant' to remove it, or pass --tenant <id>."
9049
+ );
9050
+ }
9051
+ if (pinIgnored && resolvedTenant.value !== "default" && shouldEmitHuman(args)) {
9052
+ noteOnce(
9053
+ `Note: this command is platform-scoped, so it is NOT applying the tenant pinned in ${configPath()} ('${resolvedTenant.value}'). Pass --tenant to force one.`
8768
9054
  );
8769
9055
  }
9056
+ if (problem !== null && shouldEmitHuman(args)) {
9057
+ noteOnce(`Note: not sending an unusable tenant selector \u2014 ${problem}`);
9058
+ }
9059
+ recordTenantSelection({
9060
+ value: resolvedTenant.source === "default" ? null : resolvedTenant.value,
9061
+ source: resolvedTenant.source,
9062
+ origin: tenantSelectorOrigin(resolvedTenant.source),
9063
+ applied: tenant !== null
9064
+ });
8770
9065
  const client = new HiveClient({
8771
9066
  apiUrl: args.apiUrl ?? process.env.HIVE_API_URL ?? cfg.apiUrl,
8772
9067
  // env (HIVE_TENANT / legacy HIVE_TENANT_ID) must win over the saved config,
@@ -8781,6 +9076,19 @@ function resolvesToSelfTenant(resolved) {
8781
9076
  if (resolved.source === "default") return true;
8782
9077
  return resolved.source === "pinned" && resolved.value === "default";
8783
9078
  }
9079
+ var _notesEmitted = /* @__PURE__ */ new Set();
9080
+ function noteOnce(line) {
9081
+ if (_notesEmitted.has(line)) return;
9082
+ _notesEmitted.add(line);
9083
+ process.stderr.write(`${line}
9084
+ `);
9085
+ }
9086
+ function tenantSelectorOrigin(source) {
9087
+ if (source === "flag") return "--tenant";
9088
+ if (source === "env") return cleanString(process.env.HIVE_TENANT) ? "HIVE_TENANT" : "HIVE_TENANT_ID";
9089
+ if (source === "pinned") return configPath();
9090
+ return "no --tenant, HIVE_TENANT or pinned tenant";
9091
+ }
8784
9092
  function configureGlobalTls(argv = process.argv.slice(2)) {
8785
9093
  let savedCaFile;
8786
9094
  try {
@@ -8905,8 +9213,15 @@ function flagValueFromArgv(argv, forms) {
8905
9213
  function cleanString(value) {
8906
9214
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
8907
9215
  }
9216
+ function hasControlCharacter(value) {
9217
+ for (const char of value) {
9218
+ const code = char.codePointAt(0) ?? 0;
9219
+ if (code < 32 || code === 127) return true;
9220
+ }
9221
+ return false;
9222
+ }
8908
9223
  function assertCanonicalTenantSelector(value, source = "tenant selector") {
8909
- const trimmed2 = value.trim();
9224
+ const trimmed2 = String(value ?? "").trim();
8910
9225
  if (/^tenant:/i.test(trimmed2)) {
8911
9226
  const bare = trimmed2.slice("tenant:".length);
8912
9227
  throw commandError(
@@ -8915,19 +9230,39 @@ function assertCanonicalTenantSelector(value, source = "tenant selector") {
8915
9230
  `The 'tenant:<key>' selector form is retired (#1556). Pass the displayKey '${bare}' instead \u2014 e.g. --tenant ${bare} (or HIVE_TENANT=${bare}). [${source}]`
8916
9231
  );
8917
9232
  }
9233
+ if (trimmed2 && (hasTenantAudienceDelimiter(trimmed2) || hasControlCharacter(trimmed2))) {
9234
+ const fix = source === "pinned" ? "Run 'hive config clear --tenant', then re-pin with 'hive config use --tenant <displayKey>'." : "Pass a bare tenant displayKey (e.g. --tenant talentiq) or a tenant UUID.";
9235
+ throw commandError(
9236
+ "INVALID_TENANT_SELECTOR",
9237
+ 2,
9238
+ `'${trimmed2}' cannot be a tenant selector: a tenant displayKey never contains whitespace, a comma or a control character (the server refuses them \u2014 the key is also an OIDC audience). ${fix} [${source}]`,
9239
+ { remediation: fix }
9240
+ );
9241
+ }
8918
9242
  }
8919
9243
  function resolveTenantContext(args, cfg = readConfig()) {
9244
+ const resolved = rawTenantContext(args, cfg);
9245
+ if (resolved.source !== "default") assertCanonicalTenantSelector(resolved.value, resolved.source);
9246
+ return resolved;
9247
+ }
9248
+ function rawTenantContext(args, cfg = readConfig()) {
8920
9249
  const flag = cleanString(args.tenant);
8921
- if (flag) return canonicalTenant({ value: flag, source: "flag" });
9250
+ if (flag) return { value: flag, source: "flag" };
8922
9251
  const env = cleanString(process.env.HIVE_TENANT) ?? cleanString(process.env.HIVE_TENANT_ID);
8923
- if (env) return canonicalTenant({ value: env, source: "env" });
9252
+ if (env) return { value: env, source: "env" };
8924
9253
  const pinned = cleanString(cfg.tenant);
8925
- if (pinned) return canonicalTenant({ value: pinned, source: "pinned" });
9254
+ if (pinned) return { value: pinned, source: "pinned" };
8926
9255
  return { value: "default", source: "default" };
8927
9256
  }
8928
- function canonicalTenant(resolved) {
8929
- if (typeof resolved.value === "string") assertCanonicalTenantSelector(resolved.value, resolved.source);
8930
- return resolved;
9257
+ function describeTenantContext(args, cfg = readConfig()) {
9258
+ const resolved = rawTenantContext(args, cfg);
9259
+ if (resolved.source === "default") return { ...resolved, problem: null };
9260
+ try {
9261
+ assertCanonicalTenantSelector(resolved.value, resolved.source);
9262
+ return { ...resolved, problem: null };
9263
+ } catch (err) {
9264
+ return { ...resolved, problem: err instanceof Error ? err.message : String(err) };
9265
+ }
8931
9266
  }
8932
9267
  function resolveMissionContext(args, cfg = readConfig()) {
8933
9268
  const flag = cleanString(args.missionId) ?? cleanString(args.mission);
@@ -8955,8 +9290,14 @@ function shouldEmitHuman(args) {
8955
9290
  }
8956
9291
  function printContextNotice(args, opts = {}) {
8957
9292
  if (!shouldEmitHuman(args)) return;
8958
- const tenant = resolveTenantContext(args);
8959
- if (resolvesToSelfTenant(tenant)) {
9293
+ const tenant = describeTenantContext(args);
9294
+ if (opts.adminScope && tenant.source === "pinned") {
9295
+ process.stderr.write(`Using tenant: your self-tenant (platform-scoped command; pinned '${tenant.value}' not applied)
9296
+ `);
9297
+ } else if (tenant.problem !== null) {
9298
+ process.stderr.write(`Using tenant: your self-tenant (the ${tenant.source} selector is unusable \u2014 ${tenant.problem})
9299
+ `);
9300
+ } else if (resolvesToSelfTenant(tenant)) {
8960
9301
  process.stderr.write("Using tenant: your self-tenant (no --tenant / HIVE_TENANT set)\n");
8961
9302
  } else {
8962
9303
  process.stderr.write(`Using tenant: ${tenant.value} from ${tenant.source}
@@ -9154,22 +9495,22 @@ var artifactName = injectedString(
9154
9495
  true ? "hive-cli" : void 0
9155
9496
  ) || String(process.env.HIVE_CLI_ARTIFACT_NAME || "hive-cli").trim() || "hive-cli";
9156
9497
  var packageVersion = injectedString(
9157
- true ? "1.42.1" : void 0
9498
+ true ? "1.43.1" : void 0
9158
9499
  ) || String(process.env.HIVE_CLI_PACKAGE_VERSION || "").trim() || readPackageVersion();
9159
9500
  var releaseVersion = injectedString(
9160
- true ? "1.42.1" : void 0
9501
+ true ? "1.43.1" : void 0
9161
9502
  ) || String(process.env.HIVE_CLI_RELEASE_VERSION || "").trim() || packageVersion || "dev";
9162
9503
  var releaseChannel = injectedString(
9163
9504
  true ? "final" : void 0
9164
9505
  ) || String(process.env.HIVE_CLI_RELEASE_CHANNEL || "").trim() || (releaseVersion.includes("-rc.") ? "rc" : releaseVersion === "dev" ? "dev" : "final");
9165
9506
  var buildCommit = injectedString(
9166
- true ? "13c162232e544bfb80b687d2086ba9b9c573330f" : void 0
9507
+ true ? "13ae9d3999b3acdb12e76a63041ef64c9f20d52d" : void 0
9167
9508
  ) || String(process.env.HIVE_CLI_BUILD_COMMIT || "").trim() || "unknown";
9168
9509
  var buildTimestamp = injectedString(
9169
- true ? "2026-09-22T12:45:17Z" : void 0
9510
+ true ? "2026-09-22T15:34:58Z" : void 0
9170
9511
  ) || String(process.env.HIVE_CLI_BUILD_TIMESTAMP || "").trim() || "unknown";
9171
9512
  var ociRef = injectedString(
9172
- true ? "registry.onstackit.cloud/senticor/hive-cli:1.42.1" : void 0
9513
+ true ? "registry.onstackit.cloud/senticor/hive-cli:1.43.1" : void 0
9173
9514
  ) || String(process.env.HIVE_CLI_OCI_REF || "").trim() || `registry.onstackit.cloud/senticor/${artifactName}:${releaseVersion}`;
9174
9515
  var RELEASE_INFO = Object.freeze({
9175
9516
  packageVersion,
@@ -10024,6 +10365,52 @@ init_cjs_shims();
10024
10365
  var import_node_path6 = __toESM(require("path"), 1);
10025
10366
  var DEFAULT_API_URL = "http://localhost:8787";
10026
10367
  var DEFAULT_ISSUER2 = "http://localhost:30180/realms/chos";
10368
+ function sameEndpoint(a, b) {
10369
+ const norm = (value) => String(value ?? "").trim().replace(/\/+$/, "");
10370
+ return norm(a) === norm(b);
10371
+ }
10372
+ function endpointChange(saved, apiUrl, issuerUrl) {
10373
+ if (saved.apiUrl?.trim() && !sameEndpoint(saved.apiUrl, apiUrl)) {
10374
+ return { from: saved.apiUrl.trim(), to: apiUrl };
10375
+ }
10376
+ if (saved.issuerUrl?.trim() && !sameEndpoint(saved.issuerUrl, issuerUrl)) {
10377
+ return { from: saved.issuerUrl.trim(), to: issuerUrl };
10378
+ }
10379
+ return null;
10380
+ }
10381
+ function activeMemberships(body) {
10382
+ const memberships = body?.memberships;
10383
+ if (!Array.isArray(memberships)) return null;
10384
+ const bare = (value) => typeof value === "string" && value.trim() ? value.trim().replace(/^tenant:/i, "") : null;
10385
+ return memberships.filter((row) => row?.status === "active").map((row) => {
10386
+ const membership = row;
10387
+ const displayKey = bare(membership.displayKey);
10388
+ const tenantId = bare(membership.tenantId);
10389
+ return { displayKey, selectors: [displayKey, tenantId].filter((v) => v !== null) };
10390
+ });
10391
+ }
10392
+ async function warnIfTenantIsNotAMembership(apiUrl, tenant) {
10393
+ try {
10394
+ const client = new HiveClient({ apiUrl, tenant: null });
10395
+ const me = await client.get("/api/me");
10396
+ remoteFailureHandled();
10397
+ if (me.status !== 200) return;
10398
+ const memberships = activeMemberships(me.data);
10399
+ if (memberships === null) return;
10400
+ const selector = tenant.value.trim();
10401
+ const matches = (value) => value.toLowerCase() === selector.toLowerCase();
10402
+ if (memberships.some((membership) => membership.selectors.some(matches))) return;
10403
+ const source = tenant.source === "saved" ? "pinned" : tenant.source;
10404
+ const keys = memberships.map((membership) => membership.displayKey).filter((key) => key !== null);
10405
+ const have = keys.length ? keys.join(", ") : "none";
10406
+ console.error(
10407
+ `
10408
+ \x1B[33mWarning:\x1B[0m tenant '${selector}' (${source}) is not one of your active memberships on ${apiUrl} (you have: ${have}).
10409
+ Every command will fail with TENANT_NOT_ALLOWED until you fix it: run 'hive config clear --tenant', or 'hive config use --tenant=<displayKey>' to pin one of the above.`
10410
+ );
10411
+ } catch {
10412
+ }
10413
+ }
10027
10414
  var loginArgs = {
10028
10415
  ...globalArgs,
10029
10416
  username: { type: "positional", required: false, description: "Username (pre-fills the browser login)" },
@@ -10063,10 +10450,13 @@ var login = defineCommand({
10063
10450
  issuer = await resolveSetting({ saved: cfg.issuerUrl, def: DEFAULT_ISSUER2, label: "OIDC issuer URL" });
10064
10451
  }
10065
10452
  }
10453
+ const moved = endpointChange(cfg, api.value, issuer.value);
10454
+ const savedPin = cfg.tenant?.trim() ?? "";
10455
+ const droppedPin = moved !== null && savedPin !== "" ? savedPin : null;
10066
10456
  const tenant = await resolveSetting({
10067
10457
  flag: args.tenant,
10068
10458
  envVar: ["HIVE_TENANT", "HIVE_TENANT_ID"],
10069
- saved: cfg.tenant,
10459
+ saved: moved ? void 0 : cfg.tenant,
10070
10460
  def: "default",
10071
10461
  label: "Tenant"
10072
10462
  });
@@ -10075,6 +10465,7 @@ var login = defineCommand({
10075
10465
  }
10076
10466
  const caFileRaw = String(args.caFile ?? process.env.HIVE_CA_FILE ?? "").trim();
10077
10467
  const caFile = caFileRaw ? import_node_path6.default.resolve(caFileRaw) : "";
10468
+ if (droppedPin && tenant.source === "default") clearConfig(["tenant"]);
10078
10469
  saveConfig({
10079
10470
  apiUrl: api.value,
10080
10471
  issuerUrl: issuer.value,
@@ -10086,6 +10477,11 @@ var login = defineCommand({
10086
10477
  console.log(` Issuer: ${issuer.value} \x1B[90m(${issuer.source})\x1B[0m`);
10087
10478
  if (clientId) console.log(` Client: ${clientId}`);
10088
10479
  console.log(` Tenant: ${tenant.value} \x1B[90m(${tenant.source})\x1B[0m`);
10480
+ if (droppedPin && moved) {
10481
+ console.log(
10482
+ ` \x1B[33mCleared pinned tenant '${droppedPin}'\x1B[0m \u2014 it was set for ${moved.from}, and this login is against ${moved.to}. Re-pin with 'hive config use --tenant=<displayKey>' (${configPath()}).`
10483
+ );
10484
+ }
10089
10485
  console.log("");
10090
10486
  const loginHint = args.username || args.user || void 0;
10091
10487
  const noBrowser = args.browser === false;
@@ -10093,6 +10489,7 @@ var login = defineCommand({
10093
10489
  await loginWithBrowser(issuer.value, { loginHint, noBrowser, clientId });
10094
10490
  const creds = getStoredCredentials();
10095
10491
  console.log(`\x1B[32mSigned in successfully${creds?.user ? ` as ${creds.user}` : ""}.\x1B[0m`);
10492
+ if (tenant.source !== "default") await warnIfTenantIsNotAMembership(api.value, tenant);
10096
10493
  } catch (err) {
10097
10494
  console.error(`
10098
10495
  \x1B[31mLogin failed:\x1B[0m ${err.message}`);
@@ -10146,7 +10543,10 @@ function configSnapshot(args) {
10146
10543
  path: configPath(),
10147
10544
  config: cfg,
10148
10545
  resolved: {
10149
- tenant: resolveTenantContext(args, cfg),
10546
+ // Describes rather than resolves (#173): `config show`/`config doctor` is
10547
+ // where an operator looks when a saved selector is the problem, so it
10548
+ // reports an unusable one (`problem`) instead of refusing to run.
10549
+ tenant: describeTenantContext(args, cfg),
10150
10550
  mission: resolveMissionContext(args, cfg)
10151
10551
  }
10152
10552
  };
@@ -10169,7 +10569,7 @@ var config = defineCommand({
10169
10569
  meta: { name: "show", description: "Show saved and resolved CLI context" },
10170
10570
  args: withArgs(),
10171
10571
  run({ args }) {
10172
- const { fmt } = ctx(args);
10572
+ const { fmt } = ctx(args, { diagnostic: true });
10173
10573
  return output(configSnapshot(args), fmt);
10174
10574
  }
10175
10575
  }),
@@ -10180,7 +10580,7 @@ var config = defineCommand({
10180
10580
  const before = readConfig();
10181
10581
  const patch2 = pickConfig(args);
10182
10582
  const after = saveConfig(patch2);
10183
- const { fmt } = ctx(args);
10583
+ const { fmt } = ctx(args, { diagnostic: true });
10184
10584
  return output({ ok: true, previous: before, current: after, changed: patch2 }, fmt);
10185
10585
  }
10186
10586
  }),
@@ -10206,7 +10606,7 @@ var config = defineCommand({
10206
10606
  if (args.caFile) keys.push("caFile");
10207
10607
  }
10208
10608
  const after = keys.length ? clearConfig(keys) : before;
10209
- const { fmt } = ctx(args);
10609
+ const { fmt } = ctx(args, { diagnostic: true });
10210
10610
  return output({ ok: true, previous: before, current: after, cleared: keys }, fmt);
10211
10611
  }
10212
10612
  }),
@@ -10214,7 +10614,7 @@ var config = defineCommand({
10214
10614
  meta: { name: "doctor", description: "Check local pinned context for obvious problems" },
10215
10615
  args: withArgs(),
10216
10616
  async run({ args }) {
10217
- const { fmt } = ctx(args);
10617
+ const { fmt } = ctx(args, { diagnostic: true });
10218
10618
  return output(configSnapshot(args), fmt);
10219
10619
  }
10220
10620
  })
@@ -10231,7 +10631,7 @@ var auth = defineCommand({
10231
10631
  meta: { name: "status", description: "Show active auth/session state" },
10232
10632
  args: withArgs(),
10233
10633
  run({ args }) {
10234
- const { fmt } = ctx(args);
10634
+ const { fmt } = ctx(args, { diagnostic: true });
10235
10635
  const creds = getStoredCredentials();
10236
10636
  const now = Date.now();
10237
10637
  const expiresInSeconds = creds?.expires_at ? Math.floor((creds.expires_at - now) / 1e3) : null;
@@ -10290,8 +10690,8 @@ var doctor = defineCommand({
10290
10690
  meta: { name: "doctor", description: "Diagnose CLI install, auth, API reachability, tenant, and mission context" },
10291
10691
  args: withArgs(),
10292
10692
  async run({ args }) {
10293
- const { client, fmt } = ctx(args);
10294
- const tenant = resolveTenantContext(args);
10693
+ const { client, fmt } = ctx(args, { diagnostic: true });
10694
+ const tenant = describeTenantContext(args);
10295
10695
  const mission2 = resolveMissionContext(args);
10296
10696
  const creds = getStoredCredentials();
10297
10697
  const checks = {
@@ -24960,24 +25360,6 @@ function date4(params) {
24960
25360
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
24961
25361
  config2(en_default());
24962
25362
 
24963
- // ../hive-api-contracts/src/admin-orgs.ts
24964
- init_cjs_shims();
24965
- function isValidCorsOrigin(value) {
24966
- let url2;
24967
- try {
24968
- url2 = new URL(value);
24969
- } catch {
24970
- return false;
24971
- }
24972
- if (url2.protocol !== "http:" && url2.protocol !== "https:") return false;
24973
- if (url2.username || url2.password) return false;
24974
- if (url2.host === "") return false;
24975
- if (url2.pathname !== "/" && url2.pathname !== "") return false;
24976
- if (url2.search || url2.hash) return false;
24977
- return url2.origin === value;
24978
- }
24979
- var ROLE_KEY_RE = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
24980
-
24981
25363
  // ../hive-api-contracts/src/mission-kafka-ingest.ts
24982
25364
  init_cjs_shims();
24983
25365
  var MISSION_KAFKA_SASL_MECHANISMS = [
@@ -25923,7 +26305,7 @@ function platformAdminLeaf(name, description) {
25923
26305
  meta: { name, description },
25924
26306
  args: adminArgs,
25925
26307
  async run({ args }) {
25926
- const { client, fmt } = ctx(args);
26308
+ const { client, fmt } = ctx(args, { adminScope: true });
25927
26309
  if (name === "list") {
25928
26310
  return output((await client.get("/api/admin/platform-admins")).data, fmt);
25929
26311
  }
@@ -25972,15 +26354,10 @@ var orgArgs = withArgs({
25972
26354
  });
25973
26355
  function ensureOk(status2, data, action) {
25974
26356
  if (isHttpOk(status2)) return;
25975
- const remediation = remoteFailureRemediation(data);
25976
- throw commandError(
25977
- "REMOTE_FAILED",
25978
- // #124: 401/403 and 404 carry their own contract codes; everything else
25979
- // stays the generic remote failure this always threw.
25980
- exitCodeForStatus(status2),
25981
- `${action} failed${status2 ? ` (HTTP ${status2})` : ""}: ${remoteFailureText(data)}${remediation ? ` \u2014 ${remediation}` : ""}`,
25982
- remediation ? { remediation } : void 0
25983
- );
26357
+ throw remoteFailureError(status2, data, action);
26358
+ }
26359
+ function adminCtx(args) {
26360
+ return ctx(args, { adminScope: true });
25984
26361
  }
25985
26362
  function applyRuntimeConfig(args, body) {
25986
26363
  for (const [flag, clearFlag, key] of [
@@ -26041,7 +26418,7 @@ async function resolveTenantId(args) {
26041
26418
  if (!key) {
26042
26419
  throw commandError("TENANT_ID_REQUIRED", 1, "Pass --tenant-id <uuid> or --tenant-key/--display-key <tenant>.");
26043
26420
  }
26044
- const { client } = ctx(args);
26421
+ const { client } = adminCtx(args);
26045
26422
  const res = await client.get("/api/admin/orgs");
26046
26423
  ensureOk(res.status, res.data, "list tenants");
26047
26424
  const matches = tenantList(res.data).filter(
@@ -26106,7 +26483,7 @@ var org = defineCommand({
26106
26483
  meta: { name: "list", description: "List admin-visible tenants" },
26107
26484
  args: orgArgs,
26108
26485
  async run({ args }) {
26109
- const { client, fmt } = ctx(args);
26486
+ const { client, fmt } = adminCtx(args);
26110
26487
  const res = await client.get("/api/admin/orgs");
26111
26488
  ensureOk(res.status, res.data, "list tenants");
26112
26489
  return output(res.data, fmt);
@@ -26116,8 +26493,8 @@ var org = defineCommand({
26116
26493
  meta: { name: "create", description: "Create or reuse an org/customer tenant (--display-key, [--reuse], [--pin])" },
26117
26494
  args: orgArgs,
26118
26495
  async run({ args }) {
26119
- const { client, fmt } = ctx(args);
26120
- printContextNotice(args);
26496
+ const { client, fmt } = adminCtx(args);
26497
+ printContextNotice(args, { adminScope: true });
26121
26498
  req(args, "displayKey");
26122
26499
  assertCanonicalTenantSelector(String(args.displayKey), "--display-key");
26123
26500
  const body = {
@@ -26177,7 +26554,7 @@ var org = defineCommand({
26177
26554
  args: orgArgs,
26178
26555
  async run({ args }) {
26179
26556
  const { tenantId } = await resolveTenantId(args);
26180
- const { client, fmt } = ctx(args);
26557
+ const { client, fmt } = adminCtx(args);
26181
26558
  const res = await client.get(`/api/admin/orgs/${enc(tenantId)}/members`);
26182
26559
  ensureOk(res.status, res.data, "list tenant members");
26183
26560
  return output(res.data, fmt);
@@ -26187,7 +26564,7 @@ var org = defineCommand({
26187
26564
  meta: { name: "admit", description: "Admit a user/principal by email and optional roles" },
26188
26565
  args: orgArgs,
26189
26566
  async run({ args }) {
26190
- printContextNotice(args);
26567
+ printContextNotice(args, { adminScope: true });
26191
26568
  const { tenantId } = await resolveTenantId(args);
26192
26569
  req(args, "email");
26193
26570
  const body = {
@@ -26196,7 +26573,7 @@ var org = defineCommand({
26196
26573
  };
26197
26574
  if (args.displayName) body.displayName = args.displayName;
26198
26575
  if (args.kind) body.kind = args.kind;
26199
- const { client, fmt } = ctx(args);
26576
+ const { client, fmt } = adminCtx(args);
26200
26577
  const res = await client.post(`/api/admin/orgs/${enc(tenantId)}/members`, body);
26201
26578
  ensureOk(res.status, res.data, "admit tenant member");
26202
26579
  return output({ ...recordOf2(res.data), idempotent: true }, fmt);
@@ -26212,12 +26589,12 @@ var org = defineCommand({
26212
26589
  meta: { name: "grant", description: "Grant a tenant role to a principal" },
26213
26590
  args: orgArgs,
26214
26591
  async run({ args }) {
26215
- printContextNotice(args);
26592
+ printContextNotice(args, { adminScope: true });
26216
26593
  const { tenantId } = await resolveTenantId(args);
26217
26594
  req(args, "principalId");
26218
26595
  const role = rolesFromArgs(args)[0];
26219
26596
  if (!role) throw commandError("ROLE_REQUIRED", 1, "Pass --role <role-key>.");
26220
- const { client, fmt } = ctx(args);
26597
+ const { client, fmt } = adminCtx(args);
26221
26598
  const res = await client.put(`/api/admin/orgs/${enc(tenantId)}/members/${enc(args.principalId)}/roles`, { roleKey: role });
26222
26599
  ensureOk(res.status, res.data, "grant tenant role");
26223
26600
  return output({ ...recordOf2(res.data), idempotent: true }, fmt);
@@ -26227,12 +26604,12 @@ var org = defineCommand({
26227
26604
  meta: { name: "revoke", description: "Revoke a tenant role from a principal" },
26228
26605
  args: orgArgs,
26229
26606
  async run({ args }) {
26230
- printContextNotice(args);
26607
+ printContextNotice(args, { adminScope: true });
26231
26608
  const { tenantId } = await resolveTenantId(args);
26232
26609
  req(args, "principalId");
26233
26610
  const role = rolesFromArgs(args)[0];
26234
26611
  if (!role) throw commandError("ROLE_REQUIRED", 1, "Pass --role <role-key>.");
26235
- const { client, fmt } = ctx(args);
26612
+ const { client, fmt } = adminCtx(args);
26236
26613
  const res = await client.del(`/api/admin/orgs/${enc(tenantId)}/members/${enc(args.principalId)}/roles/${enc(role)}`);
26237
26614
  ensureOk(res.status, res.data, "revoke tenant role");
26238
26615
  return output(res.data, fmt);
@@ -26247,7 +26624,7 @@ var org = defineCommand({
26247
26624
  },
26248
26625
  args: orgArgs,
26249
26626
  async run({ args }) {
26250
- printContextNotice(args);
26627
+ printContextNotice(args, { adminScope: true });
26251
26628
  req(args, "displayKey");
26252
26629
  assertCanonicalTenantSelector(String(args.displayKey), "--display-key");
26253
26630
  req(args, "email");
@@ -26264,7 +26641,7 @@ var org = defineCommand({
26264
26641
  );
26265
26642
  }
26266
26643
  const kind = String(args.kind || "organization").trim();
26267
- const { client, fmt } = ctx(args);
26644
+ const { client, fmt } = adminCtx(args);
26268
26645
  const createBody = { kind, displayKey: args.displayKey };
26269
26646
  applyRuntimeConfig(args, createBody);
26270
26647
  if (args.ownerPrincipalId) createBody.ownerPrincipalId = args.ownerPrincipalId;
@@ -26619,7 +26996,7 @@ function firstNonBlank(...values) {
26619
26996
  }
26620
26997
  function authzDiagContext(args) {
26621
26998
  const cfg = readConfig();
26622
- const tenantSelection = resolveTenantContext(args, cfg);
26999
+ const tenantSelection = describeTenantContext(args, cfg);
26623
27000
  return {
26624
27001
  client: new HiveClient({
26625
27002
  apiUrl: firstNonBlank(args.apiUrl, process.env.HIVE_API_URL, cfg.apiUrl),
@@ -26666,7 +27043,10 @@ async function runAuthzDiag(args) {
26666
27043
  cliTenantSelection: {
26667
27044
  value: tenantSelection.source === "default" ? null : tenantSelection.value,
26668
27045
  source: tenantSelection.source,
26669
- applied: false
27046
+ applied: false,
27047
+ // #173: present only when the local selector could never name a tenant
27048
+ // — the answer to "why does every other command 403?".
27049
+ ...tenantSelection.problem ? { problem: tenantSelection.problem } : {}
26670
27050
  },
26671
27051
  requestId: typeof request.requestId === "string" ? request.requestId : typeof meBody.requestId === "string" ? meBody.requestId : null,
26672
27052
  tenantSelectionSource: typeof request.tenantSelectionSource === "string" ? request.tenantSelectionSource : null,
@@ -27048,13 +27428,7 @@ function parseObjectArg(value, flag) {
27048
27428
  }
27049
27429
  function assertRemoteOk(status2, data, action, exitCode = exitCodeForStatus(status2)) {
27050
27430
  if (isHttpOk(status2)) return;
27051
- const remediation = remoteFailureRemediation(data);
27052
- throw commandError(
27053
- "REMOTE_FAILED",
27054
- exitCode,
27055
- `${action} failed${status2 ? ` (HTTP ${status2})` : ""}: ${remoteFailureText(data)}${remediation ? ` \u2014 ${remediation}` : ""}`,
27056
- remediation ? { remediation } : void 0
27057
- );
27431
+ throw remoteFailureError(status2, data, action, exitCode);
27058
27432
  }
27059
27433
  var MEMBERSHIP_ERROR_CODES = {
27060
27434
  "cannot grant the owner role (single-owner v1)": "MISSION_OWNER_GRANT_FORBIDDEN",
@@ -28582,13 +28956,7 @@ var Ajv2 = ajvModule2.default ?? ajvModule2;
28582
28956
  var addFormats2 = ajvFormatsModule2.default ?? ajvFormatsModule2;
28583
28957
  function assertRemoteOk2(status2, data, action) {
28584
28958
  if (status2 && status2 >= 200 && status2 < 300) return;
28585
- const remediation = remoteFailureRemediation(data);
28586
- throw commandError(
28587
- "REMOTE_FAILED",
28588
- 3,
28589
- `${action} failed${status2 ? ` (HTTP ${status2})` : ""}: ${remoteFailureText(data)}${remediation ? ` \u2014 ${remediation}` : ""}`,
28590
- remediation ? { remediation } : void 0
28591
- );
28959
+ throw remoteFailureError(status2, data, action, EXIT_REMOTE_FAILED);
28592
28960
  }
28593
28961
  function parseMaxBytes(value, fallback = 8 * 1024 * 1024) {
28594
28962
  if (value === void 0 || value === null || value === "") return fallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senticor/hive",
3
- "version": "1.42.1",
3
+ "version": "1.43.1",
4
4
  "description": "Cognitive Hive OS — unified CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {