@senticor/hive 1.43.0 → 1.43.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/README.md +1 -0
- package/hive.cjs +566 -198
- 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
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
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
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
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
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
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
|
-
|
|
8165
|
-
|
|
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
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
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
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
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
|
-
|
|
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({
|
|
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 =
|
|
8764
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
9252
|
+
if (env) return { value: env, source: "env" };
|
|
8924
9253
|
const pinned = cleanString(cfg.tenant);
|
|
8925
|
-
if (pinned) return
|
|
9254
|
+
if (pinned) return { value: pinned, source: "pinned" };
|
|
8926
9255
|
return { value: "default", source: "default" };
|
|
8927
9256
|
}
|
|
8928
|
-
function
|
|
8929
|
-
|
|
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 =
|
|
8959
|
-
if (
|
|
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.43.
|
|
9498
|
+
true ? "1.43.2" : void 0
|
|
9158
9499
|
) || String(process.env.HIVE_CLI_PACKAGE_VERSION || "").trim() || readPackageVersion();
|
|
9159
9500
|
var releaseVersion = injectedString(
|
|
9160
|
-
true ? "1.43.
|
|
9501
|
+
true ? "1.43.2" : 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 ? "
|
|
9507
|
+
true ? "009f700dcfa16282e76cddd90f565bba6643717e" : void 0
|
|
9167
9508
|
) || String(process.env.HIVE_CLI_BUILD_COMMIT || "").trim() || "unknown";
|
|
9168
9509
|
var buildTimestamp = injectedString(
|
|
9169
|
-
true ? "2026-09-
|
|
9510
|
+
true ? "2026-09-22T17:55:04Z" : 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.43.
|
|
9513
|
+
true ? "registry.onstackit.cloud/senticor/hive-cli:1.43.2" : 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
|
-
|
|
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 =
|
|
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
|
-
|
|
25976
|
-
|
|
25977
|
-
|
|
25978
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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;
|