@kutrion/cli 1.2.14 → 1.2.16

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 +12 -1
  2. package/dist/index.js +1294 -279
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10417,22 +10417,22 @@ var require_formdata = __commonJS({
10417
10417
  name = webidl.converters.USVString(name, prefix, "name");
10418
10418
  value = isBlobLike(value) ? webidl.converters.Blob(value, prefix, "value", { strict: false }) : webidl.converters.USVString(value, prefix, "value");
10419
10419
  filename = arguments.length === 3 ? webidl.converters.USVString(filename, prefix, "filename") : void 0;
10420
- const entry = makeEntry(name, value, filename);
10421
- this[kState].push(entry);
10420
+ const entry2 = makeEntry(name, value, filename);
10421
+ this[kState].push(entry2);
10422
10422
  }
10423
10423
  delete(name) {
10424
10424
  webidl.brandCheck(this, _FormData);
10425
10425
  const prefix = "FormData.delete";
10426
10426
  webidl.argumentLengthCheck(arguments, 1, prefix);
10427
10427
  name = webidl.converters.USVString(name, prefix, "name");
10428
- this[kState] = this[kState].filter((entry) => entry.name !== name);
10428
+ this[kState] = this[kState].filter((entry2) => entry2.name !== name);
10429
10429
  }
10430
10430
  get(name) {
10431
10431
  webidl.brandCheck(this, _FormData);
10432
10432
  const prefix = "FormData.get";
10433
10433
  webidl.argumentLengthCheck(arguments, 1, prefix);
10434
10434
  name = webidl.converters.USVString(name, prefix, "name");
10435
- const idx = this[kState].findIndex((entry) => entry.name === name);
10435
+ const idx = this[kState].findIndex((entry2) => entry2.name === name);
10436
10436
  if (idx === -1) {
10437
10437
  return null;
10438
10438
  }
@@ -10443,14 +10443,14 @@ var require_formdata = __commonJS({
10443
10443
  const prefix = "FormData.getAll";
10444
10444
  webidl.argumentLengthCheck(arguments, 1, prefix);
10445
10445
  name = webidl.converters.USVString(name, prefix, "name");
10446
- return this[kState].filter((entry) => entry.name === name).map((entry) => entry.value);
10446
+ return this[kState].filter((entry2) => entry2.name === name).map((entry2) => entry2.value);
10447
10447
  }
10448
10448
  has(name) {
10449
10449
  webidl.brandCheck(this, _FormData);
10450
10450
  const prefix = "FormData.has";
10451
10451
  webidl.argumentLengthCheck(arguments, 1, prefix);
10452
10452
  name = webidl.converters.USVString(name, prefix, "name");
10453
- return this[kState].findIndex((entry) => entry.name === name) !== -1;
10453
+ return this[kState].findIndex((entry2) => entry2.name === name) !== -1;
10454
10454
  }
10455
10455
  set(name, value, filename = void 0) {
10456
10456
  webidl.brandCheck(this, _FormData);
@@ -10464,16 +10464,16 @@ var require_formdata = __commonJS({
10464
10464
  name = webidl.converters.USVString(name, prefix, "name");
10465
10465
  value = isBlobLike(value) ? webidl.converters.Blob(value, prefix, "name", { strict: false }) : webidl.converters.USVString(value, prefix, "name");
10466
10466
  filename = arguments.length === 3 ? webidl.converters.USVString(filename, prefix, "name") : void 0;
10467
- const entry = makeEntry(name, value, filename);
10468
- const idx = this[kState].findIndex((entry2) => entry2.name === name);
10467
+ const entry2 = makeEntry(name, value, filename);
10468
+ const idx = this[kState].findIndex((entry3) => entry3.name === name);
10469
10469
  if (idx !== -1) {
10470
10470
  this[kState] = [
10471
10471
  ...this[kState].slice(0, idx),
10472
- entry,
10473
- ...this[kState].slice(idx + 1).filter((entry2) => entry2.name !== name)
10472
+ entry2,
10473
+ ...this[kState].slice(idx + 1).filter((entry3) => entry3.name !== name)
10474
10474
  ];
10475
10475
  } else {
10476
- this[kState].push(entry);
10476
+ this[kState].push(entry2);
10477
10477
  }
10478
10478
  }
10479
10479
  [nodeUtil.inspect.custom](depth, options) {
@@ -13753,16 +13753,16 @@ var require_env_http_proxy_agent = __commonJS({
13753
13753
  return false;
13754
13754
  }
13755
13755
  for (let i = 0; i < this.#noProxyEntries.length; i++) {
13756
- const entry = this.#noProxyEntries[i];
13757
- if (entry.port && entry.port !== port) {
13756
+ const entry2 = this.#noProxyEntries[i];
13757
+ if (entry2.port && entry2.port !== port) {
13758
13758
  continue;
13759
13759
  }
13760
- if (!/^[.*]/.test(entry.hostname)) {
13761
- if (hostname3 === entry.hostname) {
13760
+ if (!/^[.*]/.test(entry2.hostname)) {
13761
+ if (hostname3 === entry2.hostname) {
13762
13762
  return false;
13763
13763
  }
13764
13764
  } else {
13765
- if (hostname3.endsWith(entry.hostname.replace(/^\*/, ""))) {
13765
+ if (hostname3.endsWith(entry2.hostname.replace(/^\*/, ""))) {
13766
13766
  return false;
13767
13767
  }
13768
13768
  }
@@ -13774,13 +13774,13 @@ var require_env_http_proxy_agent = __commonJS({
13774
13774
  const noProxySplit = noProxyValue.split(/[,\s]/);
13775
13775
  const noProxyEntries = [];
13776
13776
  for (let i = 0; i < noProxySplit.length; i++) {
13777
- const entry = noProxySplit[i];
13778
- if (!entry) {
13777
+ const entry2 = noProxySplit[i];
13778
+ if (!entry2) {
13779
13779
  continue;
13780
13780
  }
13781
- const parsed = entry.match(/^(.+):(\d+)$/);
13781
+ const parsed = entry2.match(/^(.+):(\d+)$/);
13782
13782
  noProxyEntries.push({
13783
- hostname: (parsed ? parsed[1] : entry).toLowerCase(),
13783
+ hostname: (parsed ? parsed[1] : entry2).toLowerCase(),
13784
13784
  port: parsed ? Number.parseInt(parsed[2], 10) : 0
13785
13785
  });
13786
13786
  }
@@ -13828,7 +13828,7 @@ var require_headers = __commonJS({
13828
13828
  while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i;
13829
13829
  return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j);
13830
13830
  }
13831
- function fill(headers, object) {
13831
+ function fill2(headers, object) {
13832
13832
  if (Array.isArray(object)) {
13833
13833
  for (let i = 0; i < object.length; ++i) {
13834
13834
  const header = object[i];
@@ -14053,7 +14053,7 @@ var require_headers = __commonJS({
14053
14053
  this.#guard = "none";
14054
14054
  if (init !== void 0) {
14055
14055
  init = webidl.converters.HeadersInit(init, "Headers contructor", "init");
14056
- fill(this, init);
14056
+ fill2(this, init);
14057
14057
  }
14058
14058
  }
14059
14059
  // https://fetch.spec.whatwg.org/#dom-headers-append
@@ -14233,7 +14233,7 @@ var require_headers = __commonJS({
14233
14233
  });
14234
14234
  };
14235
14235
  module2.exports = {
14236
- fill,
14236
+ fill: fill2,
14237
14237
  // for test.
14238
14238
  compareHeaderName,
14239
14239
  Headers: Headers2,
@@ -14251,7 +14251,7 @@ var require_response = __commonJS({
14251
14251
  "node_modules/undici/lib/web/fetch/response.js"(exports2, module2) {
14252
14252
  "use strict";
14253
14253
  init_define_KT_HUB_FILES();
14254
- var { Headers: Headers2, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require_headers();
14254
+ var { Headers: Headers2, HeadersList, fill: fill2, getHeadersGuard, setHeadersGuard, setHeadersList } = require_headers();
14255
14255
  var { extractBody, cloneBody, mixinBody, hasFinalizationRegistry, streamRegistry, bodyUnusable } = require_body();
14256
14256
  var util = require_util();
14257
14257
  var nodeUtil = require("node:util");
@@ -14552,7 +14552,7 @@ var require_response = __commonJS({
14552
14552
  response[kState].statusText = init.statusText;
14553
14553
  }
14554
14554
  if ("headers" in init && init.headers != null) {
14555
- fill(response[kHeaders], init.headers);
14555
+ fill2(response[kHeaders], init.headers);
14556
14556
  }
14557
14557
  if (body) {
14558
14558
  if (nullBodyStatus.includes(response.status)) {
@@ -23900,7 +23900,7 @@ async function completeEmailCodeLogin(d) {
23900
23900
  }
23901
23901
 
23902
23902
  // src/index.ts
23903
- var import_fs5 = require("fs");
23903
+ var import_fs7 = require("fs");
23904
23904
 
23905
23905
  // src/owner-only.ts
23906
23906
  init_define_KT_HUB_FILES();
@@ -23921,7 +23921,7 @@ function writeOwnerOnly(path, data) {
23921
23921
 
23922
23922
  // src/index.ts
23923
23923
  var import_os5 = require("os");
23924
- var import_path6 = require("path");
23924
+ var import_path7 = require("path");
23925
23925
  var import_readline2 = require("readline");
23926
23926
  var import_net = require("net");
23927
23927
 
@@ -24056,45 +24056,148 @@ function auditResultLabel(raw) {
24056
24056
 
24057
24057
  // ../packages/shared/src/refresh-refusal-copy.ts
24058
24058
  init_define_KT_HUB_FILES();
24059
- var REFRESH_REFUSAL_COPY = {
24060
- refresh_hub_signed_out: { title: "This session was signed out", why: "You signed out in Kutrion Hub." },
24061
- refresh_hub_revoked: { title: "This session was signed out", why: "This app was signed out because its Kutrion Hub was removed." },
24062
- refresh_credentials_changed: {
24063
- title: "Your password changed",
24064
- why: "Your password changed, so Kutrion signed this {app} out.",
24065
- action: "Sign in again with your new password."
24066
- },
24067
- device_revoked: {
24068
- title: "This device was signed out",
24069
- why: "This {app}'s access was withdrawn from your Kutrion account.",
24070
- action: "Sign in again to re-register it."
24071
- },
24072
- account_inactive: {
24073
- title: "This account has ended",
24074
- why: "Your Kutrion account is no longer active, so this {app} was signed out.",
24075
- action: "Your administrator can restore it. Signing in again won't work until they do."
24076
- },
24077
- user_inactive: {
24078
- title: "This account has ended",
24079
- why: "Your Kutrion account is no longer active, so this {app} was signed out.",
24080
- action: "Your administrator can restore it. Signing in again won't work until they do."
24081
- },
24082
- account_expired: {
24083
- title: "This account has expired",
24084
- why: "Your Kutrion account has expired, so this {app} was signed out.",
24085
- action: "Your administrator can restore it. Signing in again won't work until they do."
24086
- },
24087
- org_mismatch: {
24088
- title: "This session was signed out",
24089
- why: "You're no longer a member of the organization this {app} was signed in to.",
24090
- action: "Sign in again to continue with your current organization."
24059
+
24060
+ // ../packages/shared/src/security-copy.ts
24061
+ init_define_KT_HUB_FILES();
24062
+ var SECURITY_COPY_EN = {
24063
+ "destinationRefusal.title": "Destination not authorized",
24064
+ "destinationRefusal.title.fault": "This destination could not be checked",
24065
+ "destinationRefusal.title.connection": "This destination cannot be authorized as written",
24066
+ "destinationRefusal.summary.named": 'Kutrion did not connect to {destination} and did not send the credential "{credential}".',
24067
+ "destinationRefusal.summary.namedSource": 'Kutrion did not connect to {destination} as the source of this transfer and did not send the credential "{credential}".',
24068
+ "destinationRefusal.summary.namedDest": 'Kutrion did not connect to {destination} as the destination of this transfer and did not send the credential "{credential}".',
24069
+ "destinationRefusal.summary.unnamed": "Kutrion did not connect to {destination} and did not send this credential.",
24070
+ "destinationRefusal.summary.unnamedSource": "Kutrion did not connect to {destination} as the source of this transfer and did not send this credential.",
24071
+ "destinationRefusal.summary.unnamedDest": "Kutrion did not connect to {destination} as the destination of this transfer and did not send this credential.",
24072
+ "destinationRefusal.allowed.list": "It may be presented to these destinations only:",
24073
+ "destinationRefusal.allowed.listInline": "It may be presented to these destinations only: {destinations}.",
24074
+ "destinationRefusal.allowed.noneFault": "Kutrion could not check where this credential may be presented.",
24075
+ "destinationRefusal.allowed.none": "No authorized destinations are recorded for this credential.",
24076
+ "destinationRefusal.action.allowList": "Where this credential may be presented is set on the credential itself. Changing it needs write access to the vault holding the credential, and the credential's current value.",
24077
+ "destinationRefusal.action.authorizeBelow": "Authorize this destination below.",
24078
+ "destinationRefusal.action.host": "The host recorded on this connection cannot be expressed as an authorized destination, so no entry on the credential can match it. A destination host must be a plain ASCII hostname, an IPv4 address, or an IPv6 address in square brackets, and must not carry a scheme, a port, a user name or a path. Correct the host on the connection, then authorize the corrected destination on the credential.",
24079
+ "destinationRefusal.action.domain": "The Windows domain recorded on this connection cannot be expressed as an authorized destination, so no entry on the credential can match it. A domain must be a plain ASCII name, either a short name such as CORP or a full name such as corp.example.com, entered on its own without a backslash, a user name or an address, or left empty if this host does not use one. Correct the domain on the connection, then authorize the corrected destination on the credential.",
24080
+ "destinationRefusal.action.fault": "This is a fault in Kutrion, not a permission you are missing. No change to the credential or the connection will resolve it. Report it with the time, the connection and the destination shown above.",
24081
+ "destinationRefusal.notARemedy": "Re-entering the credential's value here does not change where it may be presented.",
24082
+ "destinationRefusal.locator.named": "Open Kutrion on the web, go to the vault, open this credential and choose Destinations to allow {destination}. Or run `kt cred dest add`.",
24083
+ "destinationRefusal.locator.unnamed": "Open Kutrion on the web, go to the vault, open this credential and choose Destinations to allow this destination. Or run `kt cred dest add`.",
24084
+ "tlsDowngrade.title": "Transport security cannot be weakened",
24085
+ "tlsDowngrade.summary": "An existing connection's transport security cannot be weakened.",
24086
+ "tlsDowngrade.action": "Keep TLS and certificate checking on, or use a credential that has no stronger connection to this host.",
24087
+ "destinationNeed.notice": "Saved. Kutrion will refuse this connection until this credential is authorized for {target}.",
24088
+ "destinationNeed.who": "An editor, admin or owner of the vault {vault} can authorize it. It is listed as waiting under that credential's Destinations.",
24089
+ "destinationNeed.nonWriterPresave": "Kutrion will refuse this connection until this credential is authorized for this destination. An editor, admin or owner of the vault {vault} can authorize it. It is listed as waiting under that credential's Destinations.",
24090
+ "destinationNeed.badge": "Needs authorization",
24091
+ "destinationNeed.cleartext.ftp": "Plain FTP sends this password unencrypted.",
24092
+ "destinationNeed.cleartext.other": "This connection is not encrypted.",
24093
+ "destinationNeed.cleartext.ftpReaches": "This credential already reaches {host} over {protocols}. Plain FTP sends this password unencrypted.",
24094
+ "destinationNeed.cleartext.otherReaches": "This credential already reaches {host} over {protocols}. This connection is not encrypted.",
24095
+ "destinationNeed.publicLinks": "{count} share link(s) or drop zone(s) on this connection will start working for anyone who holds them.",
24096
+ "destinationNeed.firstClaim": "This is the credential's first destination. After this, Kutrion presents it only to the destinations listed for it.",
24097
+ "authorizeSheet.action": "Authorize this destination",
24098
+ "authorizeSheet.alsoAuthorize": "Also authorize this credential for {target}",
24099
+ "authorizeSheet.typedHint": "Changing where a credential may be sent needs the credential's own value, not just write access to the vault.",
24100
+ "authorizeSheet.secretMismatch": "That is not this credential's current value.",
24101
+ "authorizeSheet.injectOnly": "This credential is injection-only, so it cannot be authorized for a destination here.",
24102
+ "authorizeSheet.unsupported": "This server cannot authorize with the saved value. Enter the value instead.",
24103
+ "authorizeSheet.failed": "This destination could not be authorized.",
24104
+ "waiting.requestedBy": "Requested by {requester} ({role}).",
24105
+ "refreshRefusal.noun.app": "app",
24106
+ "refreshRefusal.noun.browser": "browser",
24107
+ "refreshRefusal.noun.device": "device",
24108
+ "refreshRefusal.signedOut.title": "This session was signed out",
24109
+ "refreshRefusal.refresh_hub_signed_out.why": "You signed out in Kutrion Hub.",
24110
+ "refreshRefusal.refresh_hub_revoked.why": "This {app} was signed out because its Kutrion Hub was removed.",
24111
+ "refreshRefusal.refresh_credentials_changed.title": "Your password changed",
24112
+ "refreshRefusal.refresh_credentials_changed.why": "Your password changed, so Kutrion signed this {app} out.",
24113
+ "refreshRefusal.refresh_credentials_changed.action": "Sign in again with your new password.",
24114
+ "refreshRefusal.device_revoked.title": "This device was signed out",
24115
+ "refreshRefusal.device_revoked.why": "This {app}'s access was withdrawn from your Kutrion account.",
24116
+ "refreshRefusal.device_revoked.action": "Sign in again to re-register it.",
24117
+ "refreshRefusal.account_inactive.title": "This account has ended",
24118
+ "refreshRefusal.account_inactive.why": "Your Kutrion account is no longer active, so this {app} was signed out.",
24119
+ "refreshRefusal.accountEnded.action": "Your administrator can restore it. Signing in again won't work until they do.",
24120
+ "refreshRefusal.account_expired.title": "This account has expired",
24121
+ "refreshRefusal.account_expired.why": "Your Kutrion account has expired, so this {app} was signed out.",
24122
+ "refreshRefusal.org_mismatch.why": "You're no longer a member of the organization this {app} was signed in to.",
24123
+ "refreshRefusal.org_mismatch.action": "Sign in again to continue with your current organization.",
24124
+ "privateTarget.connect.connectorAvailable": "{host} is on a private network, which hosted Kutrion cannot reach directly. Install the Kutrion Connector on that network, or use self-hosted Kutrion.",
24125
+ "privateTarget.connect.connectorPending": "{host} is on a private network, which hosted Kutrion cannot reach directly. Self-hosted Kutrion connects to hosts on private networks by default. The Kutrion Connector is not available yet.",
24126
+ "privateTarget.save.connectorAvailable": "{host} is on a private network, which hosted Kutrion cannot reach directly. Connecting to it will be refused until the Kutrion Connector is installed on that network, or you use self-hosted Kutrion.",
24127
+ "privateTarget.save.connectorPending": "{host} is on a private network, which hosted Kutrion cannot reach directly. Connecting to it will be refused on hosted Kutrion. Self-hosted Kutrion connects to hosts on private networks by default. The Kutrion Connector is not available yet.",
24128
+ "privateTarget.short.connectorAvailable": "{host} is on a private network that hosted Kutrion cannot reach directly. Install the Kutrion Connector there, or use self-hosted Kutrion.",
24129
+ "privateTarget.short.connectorPending": "{host} is on a private network that hosted Kutrion cannot reach directly. Use self-hosted Kutrion; the Kutrion Connector is not available yet.",
24130
+ "privateTarget.orgPolicy": "This organization's policy does not allow connections to {host}. Ask an organization owner or admin."
24131
+ };
24132
+ var fill = (template, values) => values ? template.replace(/\{(\w+)\}/g, (m, name) => name in values ? String(values[name]) : m) : template;
24133
+ var englishT = (key, values) => fill(SECURITY_COPY_EN[key], values);
24134
+ function securityCopy(key, t, values) {
24135
+ if (t) {
24136
+ try {
24137
+ const out = t(key, values);
24138
+ if (out && out !== key) return out;
24139
+ } catch {
24140
+ }
24091
24141
  }
24142
+ return englishT(key, values);
24143
+ }
24144
+
24145
+ // ../packages/shared/src/refresh-refusal-copy.ts
24146
+ var CODES = [
24147
+ "refresh_hub_signed_out",
24148
+ "refresh_hub_revoked",
24149
+ "refresh_credentials_changed",
24150
+ "device_revoked",
24151
+ "account_inactive",
24152
+ "user_inactive",
24153
+ "account_expired",
24154
+ "org_mismatch"
24155
+ ];
24156
+ var en = SECURITY_COPY_EN;
24157
+ var KEY_CODE = { user_inactive: "account_inactive" };
24158
+ var TITLE_KEY = {
24159
+ refresh_hub_signed_out: "signedOut",
24160
+ refresh_hub_revoked: "signedOut",
24161
+ org_mismatch: "signedOut"
24162
+ };
24163
+ var ACTION_KEY = { account_inactive: "accountEnded", account_expired: "accountEnded" };
24164
+ var keyFor = (code, part) => {
24165
+ const base = KEY_CODE[code] ?? code;
24166
+ const shared = part === "title" ? TITLE_KEY[base] : part === "action" ? ACTION_KEY[base] : void 0;
24167
+ return `refreshRefusal.${shared ?? base}.${part}`;
24092
24168
  };
24093
- function refreshRefusalCopy(code, noun = "app") {
24169
+ var entry = (code) => {
24170
+ const action = en[keyFor(code, "action")];
24171
+ return { title: en[keyFor(code, "title")], why: en[keyFor(code, "why")], ...action ? { action } : {} };
24172
+ };
24173
+ var REFRESH_REFUSAL_COPY = Object.fromEntries(
24174
+ CODES.map((code) => [code, entry(code)])
24175
+ );
24176
+ var NOUNS = ["app", "browser", "device"];
24177
+ function refreshRefusalCopy(code, noun = "app", t) {
24094
24178
  if (typeof code !== "string" || !Object.prototype.hasOwnProperty.call(REFRESH_REFUSAL_COPY, code)) return null;
24095
- const c = REFRESH_REFUSAL_COPY[code];
24096
- const fill = (s) => s.replace(/\{app\}/g, noun);
24097
- return { title: c.title, why: fill(c.why), ...c.action ? { action: fill(c.action) } : {} };
24179
+ const key = (part) => keyFor(code, part);
24180
+ const word = NOUNS.includes(noun) ? securityCopy(`refreshRefusal.noun.${noun}`, t) : noun;
24181
+ const values = { app: word };
24182
+ const hasAction = keyFor(code, "action") in SECURITY_COPY_EN;
24183
+ return {
24184
+ title: securityCopy(key("title"), t),
24185
+ why: securityCopy(key("why"), t, values),
24186
+ ...hasAction ? { action: securityCopy(key("action"), t, values) } : {}
24187
+ };
24188
+ }
24189
+
24190
+ // ../packages/shared/src/private-target-refusal.ts
24191
+ init_define_KT_HUB_FILES();
24192
+ var CONNECTOR_AVAILABLE = false;
24193
+ var privateTargetReason = (v) => v === "org_policy" ? "org_policy" : "deployment";
24194
+ function privateTargetKey(surface, reason, connectorAvailable = CONNECTOR_AVAILABLE) {
24195
+ if (reason === "org_policy") return "privateTarget.orgPolicy";
24196
+ const variant = connectorAvailable ? "connectorAvailable" : "connectorPending";
24197
+ return `privateTarget.${surface}.${variant}`;
24198
+ }
24199
+ function privateTargetSentence(o, t) {
24200
+ return securityCopy(privateTargetKey(o.surface, privateTargetReason(o.reason), o.connectorAvailable), t, { host: o.host });
24098
24201
  }
24099
24202
 
24100
24203
  // ../packages/shared/src/client.ts
@@ -27849,9 +27952,13 @@ async function withTimeout(inner, ms, input, init) {
27849
27952
  var REFRESH_BODY_MS = 3e3;
27850
27953
  var REFRESH_STORE_ATTEMPTS = 3;
27851
27954
  var REFRESH_STORE_TIMEOUT_MS = 1e4;
27955
+ var SIGNATURE_REQUIRED_COOLDOWN_MS = 3e4;
27852
27956
  function isRefreshSignatureRefusal(body) {
27853
27957
  const error = body?.error;
27854
- return error === "bad_signature" || error === "device_key_required";
27958
+ return error === "bad_signature" || error === "device_key_required" || error === "signature_required";
27959
+ }
27960
+ function isSignatureRequired(body) {
27961
+ return body?.error === "signature_required";
27855
27962
  }
27856
27963
  async function readBodyWithin(res, ms) {
27857
27964
  let timer;
@@ -28010,6 +28117,8 @@ var KutrionClient = class {
28010
28117
  // `next` is presented in its place: `spent` is already rotated, and presenting it reads as reuse.
28011
28118
  heldRefresh = null;
28012
28119
  refreshing = null;
28120
+ // performance.now() time, so a wall-clock step cannot lengthen or cut the cooldown.
28121
+ signatureRequiredUntil = 0;
28013
28122
  // VT-405 (#153): per-session key for decrypting sealed response payloads. Agreed lazily over ECDH
28014
28123
  // and held in memory only (re-agreed on demand after a reload / worker restart).
28015
28124
  sessionKey = null;
@@ -28089,6 +28198,7 @@ var KutrionClient = class {
28089
28198
  async tryRefresh() {
28090
28199
  const rt = this.presentableRefreshToken();
28091
28200
  if (!rt) return { outcome: "no_token" };
28201
+ if (!this.refreshing && performance.now() < this.signatureRequiredUntil) return { outcome: "indeterminate", status: 401 };
28092
28202
  if (!this.refreshing) {
28093
28203
  this.refreshing = (async () => {
28094
28204
  try {
@@ -28110,7 +28220,8 @@ var KutrionClient = class {
28110
28220
  }
28111
28221
  if (res.status === 401) {
28112
28222
  if (refusalBody === void 0) refusalBody = await readBodyWithin(res, REFRESH_BODY_MS);
28113
- if (signerThrew && !signed.signature && isRefreshSignatureRefusal(refusalBody)) {
28223
+ if (signerThrew && !signed.signature && isRefreshSignatureRefusal(refusalBody) || isSignatureRequired(refusalBody)) {
28224
+ if (!signerThrew && isSignatureRequired(refusalBody)) this.signatureRequiredUntil = performance.now() + SIGNATURE_REQUIRED_COOLDOWN_MS;
28114
28225
  return { outcome: "indeterminate", status: 401 };
28115
28226
  }
28116
28227
  const code = refusalBody?.error;
@@ -28499,7 +28610,10 @@ var KutrionClient = class {
28499
28610
  // losing its keys; the server revokes that row if it is the caller's and ignores it otherwise.
28500
28611
  async registerDevice(input) {
28501
28612
  const reg = await this.req("POST", "/auth/devices/register", input);
28502
- if (reg.refresh_key_bound) this.refreshSigningRefused = false;
28613
+ if (reg.refresh_key_bound) {
28614
+ this.refreshSigningRefused = false;
28615
+ this.signatureRequiredUntil = 0;
28616
+ }
28503
28617
  return reg;
28504
28618
  }
28505
28619
  // #301: mint a second refresh-token family for an existing device, owned by a companion process
@@ -28523,8 +28637,11 @@ var KutrionClient = class {
28523
28637
  try {
28524
28638
  return await this.req("POST", "/auth/refresh", body, false);
28525
28639
  } catch (e) {
28526
- if (signerThrew && !body.signature && e instanceof ApiError && e.status === 401 && isRefreshSignatureRefusal(e.body)) {
28527
- throw new ApiError(0, "signer_unavailable");
28640
+ if (e instanceof ApiError && e.status === 401) {
28641
+ if (isSignatureRequired(e.body) && !signerThrew) throw new ApiError(0, "signature_required");
28642
+ if (signerThrew && !body.signature && isRefreshSignatureRefusal(e.body) || isSignatureRequired(e.body)) {
28643
+ throw new ApiError(0, "signer_unavailable");
28644
+ }
28528
28645
  }
28529
28646
  throw e;
28530
28647
  }
@@ -28544,7 +28661,10 @@ var KutrionClient = class {
28544
28661
  refreshKey ? { device_id: deviceId, signature, refresh_key: refreshKey } : { device_id: deviceId, signature },
28545
28662
  false
28546
28663
  );
28547
- if (tokens.refresh_key_bound) this.refreshSigningRefused = false;
28664
+ if (tokens.refresh_key_bound) {
28665
+ this.refreshSigningRefused = false;
28666
+ this.signatureRequiredUntil = 0;
28667
+ }
28548
28668
  return tokens;
28549
28669
  }
28550
28670
  // --- local vault cache: keys (#709, epic #708) ---
@@ -28900,16 +29020,25 @@ var KutrionClient = class {
28900
29020
  // `port` omitted means this protocol's default, resolved and stored server-side — NOT a wildcard, and
28901
29021
  // not something to fill in here: a client-supplied default would come back as a chosen port.
28902
29022
  //
28903
- // `secret` is the credential's current plaintext and is required by design. Write on the vault gates
28904
- // who may ask; the secret is what makes a repoint cost as much as knowing the credential, which is
28905
- // the whole point of the control. Injection-only credentials cannot satisfy it and answer 403
28906
- // `inject_only`; a wrong secret answers 403 `secret_mismatch` and is rate-limited to 429 after 10.
29023
+ // Write on the vault gates who may ask; the proof gates whether they may move the credential. It is
29024
+ // one of two (#3466):
29025
+ // - `secret`, the credential's current plaintext. A wrong one answers 403 `secret_mismatch` and is
29026
+ // rate-limited to 429 after 10.
29027
+ // - `use_saved_value: true`, for a caller who could reveal the value right now. It answers every
29028
+ // refusal reveal would, plus 403 `step_up_required` without a fresh step-up on this session. An
29029
+ // older server answers it 400 `invalid_input` with an issue at path ["secret"].
29030
+ // Injection-only credentials satisfy neither and answer 403 `inject_only`. Either proof may answer
29031
+ // 403 `step_up_required` from the adaptive gate, or 403 `device_posture_required`.
28907
29032
  //
28908
29033
  // #1170: `domain` is RDP-only and omitting it means the credential is presented with NO domain — not
28909
29034
  // "any domain". It is the third member of the RDP destination tuple because it selects which
28910
29035
  // directory authenticates the credential, so a binding that omits it does not authorise one that
28911
29036
  // names it. Sent on any other protocol it answers 400 `domain_not_supported`; unexpressible values
28912
29037
  // answer 400 `invalid_domain`.
29038
+ //
29039
+ // #3466: `use_saved_value` in place of `secret` asks the server to vouch for the stored value itself; a
29040
+ // server without that mode answers 400 `invalid_input` on `secret`. `authorizeDestination` below is the
29041
+ // one caller that should choose between the two.
28913
29042
  addCredentialDestination(vaultId, cid, input) {
28914
29043
  return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/destinations`, input);
28915
29044
  }
@@ -29567,6 +29696,12 @@ var KutrionClient = class {
29567
29696
  listAgentOidcTrusts() {
29568
29697
  return this.req("GET", "/agents/oidc/trusts");
29569
29698
  }
29699
+ createAgentOidcTrust(input) {
29700
+ return this.req("POST", "/agents/oidc/trusts", input);
29701
+ }
29702
+ deleteAgentOidcTrust(id) {
29703
+ return this.req("DELETE", `/agents/oidc/trusts/${encodeURIComponent(id)}`);
29704
+ }
29570
29705
  setAgentOidcTrustResourceScope(id, scope, opts2 = {}) {
29571
29706
  return this.req("PUT", `/agents/oidc/trusts/${encodeURIComponent(id)}/resource-scope`, { ...scope, ...opts2 });
29572
29707
  }
@@ -29693,8 +29828,9 @@ var KutrionClient = class {
29693
29828
  }
29694
29829
  // Pre-flight risk guardrail (#141): a high-risk command 403s with { error: "step_up_required" }
29695
29830
  // until the session completes WebAuthn step-up; retry after stepping up.
29696
- exec(connId, command) {
29697
- return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/exec`, { command });
29831
+ // #3515: `stdinB64` is streamed to the command and then closed; the server acknowledges it with `stdin`.
29832
+ exec(connId, command, stdinB64) {
29833
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/exec`, stdinB64 === void 0 ? { command } : { command, stdin_b64: stdinB64 });
29698
29834
  }
29699
29835
  validateCommand(command) {
29700
29836
  return this.req("POST", "/ssh/validate", { command });
@@ -29711,7 +29847,7 @@ var KutrionClient = class {
29711
29847
  *
29712
29848
  * `rdp.tunnel` is deliberately not mintable here — the RDP broker returns its own on the
29713
29849
  * session-open response (`tunnel.ws_ticket`), and this route refuses the scope so a caller without
29714
- * the `rdp_broker` entitlement cannot mint against that socket (server/src/modules/ssh.ts:660).
29850
+ * the `rdp_broker` entitlement cannot mint against that socket (server/src/modules/ssh.ts:665).
29715
29851
  */
29716
29852
  mintWsTicket(scope, resourceId) {
29717
29853
  return this.req(
@@ -30113,9 +30249,20 @@ var KutrionClient = class {
30113
30249
  listFileConnections() {
30114
30250
  return this.req("GET", "/files/connections");
30115
30251
  }
30252
+ // #3466: what a create with these fields would report as `destination_need`, before creating it.
30253
+ // `connection_id` counts the public links on that connection, and only when it is the caller's own.
30254
+ // `secure` is optional and defaults as create does: true only when protocol is WEBDAV.
30255
+ fileConnectionDestinationNeed(q) {
30256
+ const qs = new URLSearchParams({ credential_id: q.credential_id, protocol: q.protocol, port: String(q.port) });
30257
+ if (q.host !== void 0) qs.set("host", q.host);
30258
+ if (q.secure !== void 0) qs.set("secure", String(q.secure));
30259
+ if (q.connection_id !== void 0) qs.set("connection_id", q.connection_id);
30260
+ return this.req("GET", `/files/connections/destination-need?${qs}`);
30261
+ }
30116
30262
  createFileConnection(input) {
30117
30263
  return this.req("POST", "/files/connections", input);
30118
30264
  }
30265
+ // #3456: `username: null` switches the connection to inherit the credential's username.
30119
30266
  updateFileConnection(id, input) {
30120
30267
  return this.req("PATCH", `/files/connections/${encodeURIComponent(id)}`, input);
30121
30268
  }
@@ -30332,6 +30479,10 @@ var KutrionClient = class {
30332
30479
  patchTransferJob(id, input) {
30333
30480
  return this.req("PATCH", `/files/jobs/${encodeURIComponent(id)}`, input);
30334
30481
  }
30482
+ // #3549: cancel a queued or running job. A finished one answers 409 not_active.
30483
+ cancelTransferJob(id) {
30484
+ return this.req("POST", `/files/jobs/${encodeURIComponent(id)}/cancel`, {});
30485
+ }
30335
30486
  // #563: recurring transfer schedules (interval or daily UTC time).
30336
30487
  createSchedule(input) {
30337
30488
  return this.req("POST", "/files/schedules", input);
@@ -30958,50 +31109,65 @@ function parseDestinationRefusal(body) {
30958
31109
  reason: DESTINATION_REFUSAL_REASONS.includes(b.reason) ? b.reason : null
30959
31110
  };
30960
31111
  }
30961
- var DESTINATION_REFUSAL_TITLE = "Destination not authorized";
30962
- var subject = (r) => r.credential ? `the credential "${r.credential}"` : "this credential";
30963
- function destinationRefusalSummary(r) {
30964
- const where = r.side === "source" ? " as the source of this transfer" : r.side === "dest" ? " as the destination of this transfer" : "";
30965
- return `Kutrion did not connect to ${r.destination}${where} and did not send ${subject(r)}.`;
30966
- }
30967
- function destinationRefusalAllowedLabel(r) {
30968
- if (r.allowedDestinations.length) return "It may be presented to these destinations only:";
30969
- if (r.detail) return r.detail;
30970
- return destinationRefusalFault(r) === "kutrion" ? "Kutrion could not check where this credential may be presented." : "No authorized destinations are recorded for this credential.";
30971
- }
30972
- var DESTINATION_REFUSAL_ACTION = "Where this credential may be presented is set on the credential itself. Changing it needs write access to the vault holding the credential, and the credential's current value.";
30973
- var DESTINATION_REFUSAL_NOT_A_REMEDY = "Re-entering the credential's value here does not change where it may be presented.";
30974
- var DESTINATION_REFUSAL_FAULT_TITLE = "This destination could not be checked";
30975
- var DESTINATION_REFUSAL_FAULT_ACTION = "This is a fault in Kutrion, not a permission you are missing. No change to the credential or the connection will resolve it. Report it with the time, the connection and the destination shown above.";
30976
- var DESTINATION_REFUSAL_CONNECTION_TITLE = "This destination cannot be authorized as written";
30977
- var DESTINATION_REFUSAL_HOST_ACTION = "The host recorded on this connection cannot be expressed as an authorized destination, so no entry on the credential can match it. A destination host must be a plain ASCII hostname, an IPv4 address, or an IPv6 address in square brackets, and must not carry a scheme, a port, a user name or a path. Correct the host on the connection, then authorize the corrected destination on the credential.";
30978
- var DESTINATION_REFUSAL_DOMAIN_ACTION = "The Windows domain recorded on this connection cannot be expressed as an authorized destination, so no entry on the credential can match it. A domain must be a plain ASCII name, either a short name such as CORP or a full name such as corp.example.com, entered on its own without a backslash, a user name or an address, or left empty if this host does not use one. Correct the domain on the connection, then authorize the corrected destination on the credential.";
30979
- function destinationRefusalTitle(r) {
31112
+ var DESTINATION_REFUSAL_TITLE = SECURITY_COPY_EN["destinationRefusal.title"];
31113
+ var usSpelling = (s) => s.replace(/\b(A|a)uthoris/g, "$1uthoriz");
31114
+ function destinationRefusalSummary(r, t) {
31115
+ const side = r.side === "source" ? "Source" : r.side === "dest" ? "Dest" : "";
31116
+ const key = `destinationRefusal.summary.${r.credential ? "named" : "unnamed"}${side}`;
31117
+ return securityCopy(key, t, { destination: r.destination, credential: r.credential ?? "" });
31118
+ }
31119
+ function destinationRefusalAllowedLabel(r, t) {
31120
+ if (r.allowedDestinations.length) return securityCopy("destinationRefusal.allowed.list", t);
31121
+ if (r.detail) return usSpelling(r.detail);
31122
+ return securityCopy(destinationRefusalFault(r) === "kutrion" ? "destinationRefusal.allowed.noneFault" : "destinationRefusal.allowed.none", t);
31123
+ }
31124
+ var DESTINATION_REFUSAL_ACTION = SECURITY_COPY_EN["destinationRefusal.action.allowList"];
31125
+ var isAllowListRemedy = (r) => r.reason !== "host_not_normalizable" && r.reason !== "domain_not_normalizable" && destinationRefusalFault(r) !== "kutrion";
31126
+ var DESTINATION_REFUSAL_NOT_A_REMEDY = SECURITY_COPY_EN["destinationRefusal.notARemedy"];
31127
+ var DESTINATION_REFUSAL_FAULT_TITLE = SECURITY_COPY_EN["destinationRefusal.title.fault"];
31128
+ var DESTINATION_REFUSAL_FAULT_ACTION = SECURITY_COPY_EN["destinationRefusal.action.fault"];
31129
+ var DESTINATION_REFUSAL_CONNECTION_TITLE = SECURITY_COPY_EN["destinationRefusal.title.connection"];
31130
+ var DESTINATION_REFUSAL_HOST_ACTION = SECURITY_COPY_EN["destinationRefusal.action.host"];
31131
+ var DESTINATION_REFUSAL_DOMAIN_ACTION = SECURITY_COPY_EN["destinationRefusal.action.domain"];
31132
+ function destinationRefusalTitle(r, t) {
30980
31133
  if (r.reason === "host_not_normalizable" || r.reason === "domain_not_normalizable") {
30981
- return DESTINATION_REFUSAL_CONNECTION_TITLE;
31134
+ return securityCopy("destinationRefusal.title.connection", t);
30982
31135
  }
30983
- return destinationRefusalFault(r) === "kutrion" ? DESTINATION_REFUSAL_FAULT_TITLE : DESTINATION_REFUSAL_TITLE;
31136
+ return securityCopy(destinationRefusalFault(r) === "kutrion" ? "destinationRefusal.title.fault" : "destinationRefusal.title", t);
30984
31137
  }
30985
- function destinationRefusalAction(r) {
30986
- if (r.reason === "host_not_normalizable") return DESTINATION_REFUSAL_HOST_ACTION;
30987
- if (r.reason === "domain_not_normalizable") return DESTINATION_REFUSAL_DOMAIN_ACTION;
30988
- return destinationRefusalFault(r) === "kutrion" ? DESTINATION_REFUSAL_FAULT_ACTION : DESTINATION_REFUSAL_ACTION;
31138
+ function destinationRefusalAction(r, o, t) {
31139
+ if (o?.inlineAction && isAllowListRemedy(r)) return securityCopy("destinationRefusal.action.authorizeBelow", t);
31140
+ if (r.reason === "host_not_normalizable") return securityCopy("destinationRefusal.action.host", t);
31141
+ if (r.reason === "domain_not_normalizable") return securityCopy("destinationRefusal.action.domain", t);
31142
+ return securityCopy(destinationRefusalFault(r) === "kutrion" ? "destinationRefusal.action.fault" : "destinationRefusal.action.allowList", t);
30989
31143
  }
30990
- function destinationRefusalLines(r, indent = " ") {
31144
+ function destinationRefusalLines(r, indent = " ", t) {
30991
31145
  return [
30992
- destinationRefusalTitle(r),
31146
+ destinationRefusalTitle(r, t),
30993
31147
  "",
30994
- destinationRefusalSummary(r),
31148
+ destinationRefusalSummary(r, t),
30995
31149
  "",
30996
- destinationRefusalAllowedLabel(r),
31150
+ destinationRefusalAllowedLabel(r, t),
30997
31151
  ...r.allowedDestinations.map((d) => `${indent}${d}`),
30998
31152
  "",
30999
- destinationRefusalAction(r),
31153
+ destinationRefusalAction(r, void 0, t),
31000
31154
  // Only true of a refusal that turned on where the credential may go. On a fault of ours the secret
31001
31155
  // was never the subject, and the line reads as one more thing the user got wrong.
31002
- ...destinationRefusalFault(r) === "kutrion" ? [] : [DESTINATION_REFUSAL_NOT_A_REMEDY]
31156
+ ...destinationRefusalFault(r) === "kutrion" ? [] : [securityCopy("destinationRefusal.notARemedy", t)]
31003
31157
  ];
31004
31158
  }
31159
+ var TLS_DOWNGRADE_REFUSAL_TITLE = SECURITY_COPY_EN["tlsDowngrade.title"];
31160
+ var TLS_DOWNGRADE_REFUSAL_SUMMARY = SECURITY_COPY_EN["tlsDowngrade.summary"];
31161
+ var TLS_DOWNGRADE_REFUSAL_ACTION = SECURITY_COPY_EN["tlsDowngrade.action"];
31162
+ var DESTINATION_NEED_BADGE = SECURITY_COPY_EN["destinationNeed.badge"];
31163
+ var DESTINATION_REFUSAL_AUTHORIZE_BELOW = SECURITY_COPY_EN["destinationRefusal.action.authorizeBelow"];
31164
+ var DESTINATION_AUTHORIZE_ACTION = SECURITY_COPY_EN["authorizeSheet.action"];
31165
+ var DESTINATION_TYPED_HINT = SECURITY_COPY_EN["authorizeSheet.typedHint"];
31166
+ var DESTINATION_SECRET_MISMATCH = SECURITY_COPY_EN["authorizeSheet.secretMismatch"];
31167
+ var DESTINATION_INJECT_ONLY = SECURITY_COPY_EN["authorizeSheet.injectOnly"];
31168
+ var DESTINATION_UNSUPPORTED = SECURITY_COPY_EN["authorizeSheet.unsupported"];
31169
+ var DESTINATION_FAILED = SECURITY_COPY_EN["authorizeSheet.failed"];
31170
+ var DESTINATION_FIRST_CLAIM_WARNING = SECURITY_COPY_EN["destinationNeed.firstClaim"];
31005
31171
 
31006
31172
  // ../packages/shared/src/credential-size-refusal.ts
31007
31173
  init_define_KT_HUB_FILES();
@@ -31163,6 +31329,16 @@ function platformFlagRefusalText(code) {
31163
31329
  return code && Object.prototype.hasOwnProperty.call(PLATFORM_FLAG_REFUSAL_COPY, code) ? PLATFORM_FLAG_REFUSAL_COPY[code] : null;
31164
31330
  }
31165
31331
 
31332
+ // ../packages/shared/src/remote-auth-refusal.ts
31333
+ init_define_KT_HUB_FILES();
31334
+ var REMOTE_AUTH_FAILED_TEXT = "The server rejected the sign-in. Check the username and password or access key saved for this connection.";
31335
+ var BASIC_OVER_HTTP_REFUSED_TEXT = "Kutrion did not send the password. The server did not offer a protected sign-in over this unencrypted connection, so the password would have been readable to anyone on the network path.";
31336
+ var BASIC_AFTER_DIGEST_REFUSED_TEXT = "Kutrion did not send the password. This server asked for Digest sign-in earlier and now asks for Basic, which would expose the password.";
31337
+ var PLAIN_HTTP_OPT_IN_LABEL = "Allow sign-in over plain HTTP";
31338
+ var BASIC_OVER_HTTP_REFUSED_NEXT = `Switch this connection to HTTPS, or tick "${PLAIN_HTTP_OPT_IN_LABEL}" on it.`;
31339
+ var CREDENTIAL_CONTROL_CHARS_TEXT = "Kutrion did not send the sign-in. The username or password saved for this connection contains a line break or NUL character, which this protocol cannot carry.";
31340
+ var CREDENTIAL_CONTROL_CHARS_NEXT = "Edit the credential in the vault so it has no line break, then open the connection again.";
31341
+
31166
31342
  // ../packages/shared/src/ssh-refusal.ts
31167
31343
  init_define_KT_HUB_FILES();
31168
31344
  var SSH_REFUSAL_CLASSIFICATION = {
@@ -31408,12 +31584,12 @@ function parseJumpRefusal(body) {
31408
31584
  const code = str4(b.code) ?? str4(b.error);
31409
31585
  if (!isJumpRefusalCode(code)) return null;
31410
31586
  const nested = b.detail && typeof b.detail === "object" ? b.detail : {};
31411
- const pick2 = (k) => b[k] !== void 0 ? b[k] : nested[k];
31412
- const idx = pick2("hop_index");
31413
- const allowed = pick2("allowed_destinations");
31587
+ const pick3 = (k) => b[k] !== void 0 ? b[k] : nested[k];
31588
+ const idx = pick3("hop_index");
31589
+ const allowed = pick3("allowed_destinations");
31414
31590
  return {
31415
31591
  code,
31416
- hopConnectionId: str4(pick2("hop_connection_id")),
31592
+ hopConnectionId: str4(pick3("hop_connection_id")),
31417
31593
  hopIndex: typeof idx === "number" && Number.isFinite(idx) ? idx : null,
31418
31594
  detail: typeof b.detail === "string" ? str4(b.detail) : str4(nested.detail) ?? str4(b.error_description),
31419
31595
  allowedDestinations: Array.isArray(allowed) ? allowed.filter((x) => typeof x === "string" && x.trim() !== "") : null
@@ -31472,12 +31648,12 @@ function parseJumpHostInUse(body) {
31472
31648
  var JUMP_HOST_IN_USE_TITLE = "Other connections are reached through this one";
31473
31649
  function jumpHostInUseSummary(r, deletingName) {
31474
31650
  const name = str4(deletingName);
31475
- const subject2 = name ? `\u201C${name}\u201D` : "this connection";
31651
+ const subject = name ? `\u201C${name}\u201D` : "this connection";
31476
31652
  const n = r.usedBy.length;
31477
31653
  if (!n) {
31478
- return `${subject2} is used as a jump host by other connections, so deleting it would leave them with no route.`;
31654
+ return `${subject} is used as a jump host by other connections, so deleting it would leave them with no route.`;
31479
31655
  }
31480
- return `${subject2} is the jump host for ${n === 1 ? "another connection" : `${n} other connections`}. Deleting it would leave ` + (n === 1 ? "it" : "them") + " with no route to the target, so it was not deleted.";
31656
+ return `${subject} is the jump host for ${n === 1 ? "another connection" : `${n} other connections`}. Deleting it would leave ` + (n === 1 ? "it" : "them") + " with no route to the target, so it was not deleted.";
31481
31657
  }
31482
31658
  var JUMP_HOST_IN_USE_ACTION = "Point each of these at a different jump host, or clear the jump host to reach it directly, and then delete this connection.";
31483
31659
 
@@ -32567,6 +32743,12 @@ function subtle() {
32567
32743
  if (!s || typeof globalThis.crypto.getRandomValues !== "function") throw new VaultKeyError("crypto_unavailable");
32568
32744
  return s;
32569
32745
  }
32746
+ var random = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
32747
+ function toB64(bytes) {
32748
+ let s = "";
32749
+ for (const b of bytes) s += String.fromCharCode(b);
32750
+ return btoa(s);
32751
+ }
32570
32752
  function fromB64(b64) {
32571
32753
  const s = atob(b64);
32572
32754
  const out = new Uint8Array(s.length);
@@ -32612,6 +32794,10 @@ async function derivePassphraseKek(passphrase, kdf, onProgress) {
32612
32794
  out.fill(0);
32613
32795
  }
32614
32796
  }
32797
+ function generateVaultKey() {
32798
+ subtle();
32799
+ return random(VAULT_KEY_BYTES);
32800
+ }
32615
32801
  async function unwrapVaultKey(wrap, kek, purpose) {
32616
32802
  let bytes;
32617
32803
  try {
@@ -32647,6 +32833,15 @@ async function unlockVaultKey(record, passphrase, onProgress) {
32647
32833
  }
32648
32834
  var dataKeyAad = (vaultId) => enc.encode(`kutrion-vault-key-v1:data-key:${vaultId.toLowerCase()}`);
32649
32835
  var TAG_BYTES = TAG_BITS / 8;
32836
+ async function wrapVaultDataKey(dataKey, vaultKey, vaultId) {
32837
+ if (dataKey.length !== VAULT_KEY_BYTES) throw new VaultKeyError("malformed_wrap", "A vault data key is 32 bytes.");
32838
+ const iv = random(IV_BYTES);
32839
+ const ct = new Uint8Array(await subtle().encrypt({ name: "AES-GCM", iv: bs2(iv), additionalData: bs2(dataKeyAad(vaultId)), tagLength: TAG_BITS }, vaultKey, bs2(dataKey)));
32840
+ const out = new Uint8Array(IV_BYTES + ct.length);
32841
+ out.set(iv, 0);
32842
+ out.set(ct, IV_BYTES);
32843
+ return toB64(out);
32844
+ }
32650
32845
  async function unwrapVaultDataKey(wrap, vaultKey, vaultId) {
32651
32846
  let bytes;
32652
32847
  try {
@@ -32670,6 +32865,14 @@ async function unwrapVaultDataKey(wrap, vaultKey, vaultId) {
32670
32865
  }
32671
32866
  }
32672
32867
  var itemAad = (vaultId, itemId, purpose) => enc.encode(`kutrion-vault-key-v1:item:${vaultId.toLowerCase()}:${itemId.toLowerCase()}${purpose ? `:${purpose}` : ""}`);
32868
+ async function sealItemField(dataKey, plaintext, vaultId, itemId, purpose) {
32869
+ const iv = random(IV_BYTES);
32870
+ const ctTag = new Uint8Array(await subtle().encrypt({ name: "AES-GCM", iv: bs2(iv), additionalData: bs2(itemAad(vaultId, itemId, purpose)), tagLength: TAG_BITS }, dataKey, bs2(enc.encode(plaintext))));
32871
+ const blob = new Uint8Array(ctTag.length);
32872
+ blob.set(ctTag.subarray(ctTag.length - TAG_BYTES), 0);
32873
+ blob.set(ctTag.subarray(0, ctTag.length - TAG_BYTES), TAG_BYTES);
32874
+ return { iv: toB64(iv), blob: toB64(blob) };
32875
+ }
32673
32876
  async function openItemField(dataKey, iv, blob, vaultId, itemId, purpose) {
32674
32877
  let ivBytes, b;
32675
32878
  try {
@@ -32728,6 +32931,40 @@ async function openPrivateItem(item, sealed, vaultKey) {
32728
32931
  ]);
32729
32932
  return { value: value ?? "", notes, custom_fields: fields ? JSON.parse(fields) : [], passphrase, totp };
32730
32933
  }
32934
+ async function privateItemDataKey(current, vaultKey, vaultId, enrolledAt) {
32935
+ if (current) {
32936
+ return { dataKey: await unwrapVaultDataKey(current.client_wrapped_data_key, vaultKey, vaultId), key: { key_id: current.key_id } };
32937
+ }
32938
+ const raw = generateVaultKey();
32939
+ try {
32940
+ const [client_wrapped_data_key, dataKey] = await Promise.all([
32941
+ wrapVaultDataKey(raw, vaultKey, vaultId),
32942
+ subtle().importKey("raw", bs2(raw), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"])
32943
+ ]);
32944
+ return { dataKey, key: { client_wrapped_data_key, vault_key_created_at: enrolledAt } };
32945
+ } finally {
32946
+ raw.fill(0);
32947
+ }
32948
+ }
32949
+ async function sealPrivateItem(item, plain, dataKey) {
32950
+ const seal = (s, purpose) => sealItemField(dataKey, s, item.vault_id, item.id, purpose);
32951
+ const opt = async (s, purpose) => s === void 0 ? void 0 : s === null || s === "" ? null : seal(s, purpose);
32952
+ const fields = plain.custom_fields === void 0 ? void 0 : plain.custom_fields && plain.custom_fields.length ? JSON.stringify(plain.custom_fields) : null;
32953
+ const [value, notes, custom_fields, passphrase, totp] = await Promise.all([
32954
+ plain.value === void 0 ? void 0 : seal(plain.value),
32955
+ opt(plain.notes),
32956
+ opt(fields),
32957
+ opt(plain.passphrase),
32958
+ opt(plain.totp?.trim() ?? plain.totp, "totp")
32959
+ ]);
32960
+ const out = {};
32961
+ if (value !== void 0) out.value = value;
32962
+ if (notes !== void 0) out.notes = notes;
32963
+ if (custom_fields !== void 0) out.custom_fields = custom_fields;
32964
+ if (passphrase !== void 0) out.passphrase = passphrase;
32965
+ if (totp !== void 0) out.totp = totp;
32966
+ return out;
32967
+ }
32731
32968
 
32732
32969
  // ../packages/shared/src/private-vault-copy.ts
32733
32970
  init_define_KT_HUB_FILES();
@@ -32812,6 +33049,7 @@ var PRIVATE_VAULT_COPY = {
32812
33049
 
32813
33050
  // src/args.ts
32814
33051
  init_define_KT_HUB_FILES();
33052
+ var REPEATABLE = /* @__PURE__ */ new Set(["vault", "connection", "scope-vault", "scope-conn"]);
32815
33053
  function parseForwardSpec(spec) {
32816
33054
  const m = spec.match(/^(\d{1,5}):(.+):(\d{1,5})$/);
32817
33055
  if (!m) return null;
@@ -32823,10 +33061,12 @@ function parseForwardSpec(spec) {
32823
33061
  return { localPort, dstHost, dstPort };
32824
33062
  }
32825
33063
  var GLOBAL_FLAGS = { base: "value", json: "boolean" };
32826
- var SWITCHES = /* @__PURE__ */ new Set(["browser", "reveal", "password-stdin", "value-stdin", "vault-passphrase-stdin", "fallback", "dry-run"]);
33064
+ var SWITCHES = /* @__PURE__ */ new Set(["browser", "reveal", "password-stdin", "value-stdin", "vault-passphrase-stdin", "fallback", "dry-run", "allow-insecure-lan", "stdin"]);
32827
33065
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32828
33066
  "action",
32829
33067
  "agent-forward",
33068
+ "allow-insecure-lan",
33069
+ "audience",
32830
33070
  "base",
32831
33071
  "browser",
32832
33072
  "client",
@@ -32838,6 +33078,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32838
33078
  "conflict",
32839
33079
  "conn",
32840
33080
  "conns",
33081
+ "connection",
32841
33082
  "cred",
32842
33083
  "current",
32843
33084
  "daily-at",
@@ -32853,7 +33094,9 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32853
33094
  "fingerprint",
32854
33095
  "format",
32855
33096
  "from",
33097
+ "help",
32856
33098
  "host",
33099
+ "issuer",
32857
33100
  "insert",
32858
33101
  "intent",
32859
33102
  "json",
@@ -32890,12 +33133,16 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32890
33133
  "reveal",
32891
33134
  "role",
32892
33135
  "scope",
33136
+ "scope-conn",
33137
+ "scope-vault",
32893
33138
  "scopes",
32894
33139
  "secret",
32895
33140
  "service",
32896
33141
  "session",
32897
33142
  "since",
32898
33143
  "status",
33144
+ "stdin",
33145
+ "subject",
32899
33146
  "target",
32900
33147
  "throttle",
32901
33148
  "timeout",
@@ -32913,13 +33160,22 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32913
33160
  "verify",
32914
33161
  "views",
32915
33162
  "wait",
33163
+ "write-env",
32916
33164
  "yes"
32917
33165
  ]);
32918
33166
  function unknownFlags(flags) {
32919
33167
  return Object.keys(flags).filter((k) => !KNOWN_FLAGS.has(k));
32920
33168
  }
33169
+ function repeatedFlag(argv2, name) {
33170
+ const out = [];
33171
+ for (let i = 0; i < argv2.length - 1; i++) {
33172
+ if (argv2[i] === `--${name}` && !argv2[i + 1].startsWith("--")) out.push(argv2[i + 1]);
33173
+ }
33174
+ return out;
33175
+ }
32921
33176
  function parseArgs(argv2) {
32922
33177
  const flags = {};
33178
+ const multi = {};
32923
33179
  let at = 0;
32924
33180
  while (at < argv2.length && argv2[at].startsWith("--")) {
32925
33181
  const key = argv2[at].slice(2);
@@ -32937,13 +33193,19 @@ function parseArgs(argv2) {
32937
33193
  const cmd = argv2[at];
32938
33194
  const rest = argv2.slice(at + 1);
32939
33195
  const positionals = [];
33196
+ if (cmd === "exec") return { cmd, positionals: parseExecArgs(rest, flags), flags, multi };
32940
33197
  for (let i = 0; i < rest.length; i++) {
32941
33198
  const a = rest[i];
33199
+ if (a === "--") {
33200
+ positionals.push(...rest.slice(i + 1));
33201
+ break;
33202
+ }
32942
33203
  if (a.startsWith("--")) {
32943
33204
  const key = a.slice(2);
32944
33205
  const next = rest[i + 1];
32945
33206
  if (!SWITCHES.has(key) && next !== void 0 && !next.startsWith("--")) {
32946
33207
  flags[key] = next;
33208
+ if (REPEATABLE.has(key)) (multi[key] ??= []).push(next);
32947
33209
  i++;
32948
33210
  } else {
32949
33211
  flags[key] = true;
@@ -32952,7 +33214,134 @@ function parseArgs(argv2) {
32952
33214
  positionals.push(a);
32953
33215
  }
32954
33216
  }
32955
- return { cmd: cmd ?? "help", positionals, flags };
33217
+ return { cmd: cmd ?? "help", positionals, flags, multi };
33218
+ }
33219
+ function parseExecArgs(rest, flags) {
33220
+ const positionals = [];
33221
+ let i = 0;
33222
+ for (; i < rest.length; i++) {
33223
+ const a = rest[i];
33224
+ if (a === "--") {
33225
+ i++;
33226
+ if (positionals.length) break;
33227
+ continue;
33228
+ }
33229
+ if (a.startsWith("--")) {
33230
+ const key = a.slice(2);
33231
+ const next = rest[i + 1];
33232
+ if (!SWITCHES.has(key) && key !== "json" && next !== void 0 && !next.startsWith("--")) {
33233
+ flags[key] = next;
33234
+ i++;
33235
+ } else {
33236
+ flags[key] = true;
33237
+ }
33238
+ } else if (!positionals.length) {
33239
+ positionals.push(a);
33240
+ } else {
33241
+ break;
33242
+ }
33243
+ }
33244
+ positionals.push(...rest.slice(i));
33245
+ return positionals;
33246
+ }
33247
+
33248
+ // src/base-fallback.ts
33249
+ init_define_KT_HUB_FILES();
33250
+ var import_fs2 = require("fs");
33251
+ var norm = (u) => u.trim().replace(/\/+$/, "");
33252
+ function splitBases(parts) {
33253
+ const out = [];
33254
+ for (const p of parts) for (const raw of p.split(",")) {
33255
+ const u = norm(raw);
33256
+ if (u && !out.includes(u)) out.push(u);
33257
+ }
33258
+ return out;
33259
+ }
33260
+ function fallbackRefusal(f) {
33261
+ let u;
33262
+ try {
33263
+ u = new URL(f.url);
33264
+ } catch {
33265
+ return `not a valid server URL: ${f.url}`;
33266
+ }
33267
+ if (u.protocol === "https:") return null;
33268
+ if (u.protocol === "http:") {
33269
+ return f.insecure ? null : `${f.url} is plain http; credentials would cross it unencrypted. Use https, or opt this base in with --allow-insecure-lan`;
33270
+ }
33271
+ return `unsupported scheme for ${f.url}`;
33272
+ }
33273
+ var CONNECTION_CODES = /* @__PURE__ */ new Set([
33274
+ "ECONNREFUSED",
33275
+ "ECONNRESET",
33276
+ "ENOTFOUND",
33277
+ "EAI_AGAIN",
33278
+ "ETIMEDOUT",
33279
+ "EHOSTUNREACH",
33280
+ "ENETUNREACH",
33281
+ "EHOSTDOWN",
33282
+ "UND_ERR_CONNECT_TIMEOUT",
33283
+ "UND_ERR_SOCKET"
33284
+ ]);
33285
+ function isConnectionFailure(e) {
33286
+ for (let cur = e, depth = 0; cur && depth < 4; cur = cur.cause, depth++) {
33287
+ if (typeof cur.code === "string" && CONNECTION_CODES.has(cur.code)) return true;
33288
+ if (cur.name === "TimeoutError") return true;
33289
+ }
33290
+ return false;
33291
+ }
33292
+ var isEdgeFailure = (status) => status >= 520 && status <= 529;
33293
+ var replayable = (b) => b == null || typeof b === "string" || b instanceof URLSearchParams || b instanceof ArrayBuffer || ArrayBuffer.isView(b);
33294
+ function fallbackFetch(inner, primary, fallbacks, warn = (l) => console.error(l)) {
33295
+ const p = norm(primary);
33296
+ const bases = [{ url: p }];
33297
+ for (const f of fallbacks) {
33298
+ const why2 = fallbackRefusal(f);
33299
+ if (why2) {
33300
+ warn(`warning: ignoring fallback base: ${why2}`);
33301
+ continue;
33302
+ }
33303
+ if (norm(f.url) !== p) bases.push({ ...f, url: norm(f.url) });
33304
+ }
33305
+ if (bases.length === 1) return inner;
33306
+ let sticky = 0;
33307
+ return async (input, init) => {
33308
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : null;
33309
+ if (url === null || !(url === p || url.startsWith(`${p}/`) || url.startsWith(`${p}?`)) || !replayable(init?.body)) {
33310
+ return inner(input, init);
33311
+ }
33312
+ const suffix = url.slice(p.length);
33313
+ const order = [sticky, ...bases.map((_, i) => i).filter((i) => i !== sticky)];
33314
+ let firstErr;
33315
+ for (let n = 0; n < order.length; n++) {
33316
+ const i = order[n];
33317
+ const b = bases[i];
33318
+ if (b.insecure) warn(`warning: ${b.url} is plain http (opted in with --allow-insecure-lan); this request, with its credentials, is not encrypted`);
33319
+ const more = n < order.length - 1;
33320
+ try {
33321
+ const res = await inner(b.url + suffix, init);
33322
+ if (isEdgeFailure(res.status) && more) {
33323
+ firstErr ??= new Error(`${b.url} answered ${res.status}`);
33324
+ continue;
33325
+ }
33326
+ if (!isEdgeFailure(res.status)) sticky = i;
33327
+ return res;
33328
+ } catch (e) {
33329
+ if (init?.signal?.aborted || !isConnectionFailure(e)) throw e;
33330
+ firstErr ??= e;
33331
+ if (!more) throw firstErr;
33332
+ }
33333
+ }
33334
+ throw firstErr;
33335
+ };
33336
+ }
33337
+ function readFallbacks(configPath, base) {
33338
+ try {
33339
+ const f = JSON.parse((0, import_fs2.readFileSync)(configPath, "utf8"));
33340
+ if (typeof f.base !== "string" || norm(f.base) !== norm(base) || !Array.isArray(f.fallbacks)) return [];
33341
+ return f.fallbacks.filter((x) => x && typeof x.url === "string").map((x) => ({ url: norm(x.url), ...x.insecure === true ? { insecure: true } : {} }));
33342
+ } catch {
33343
+ return [];
33344
+ }
32956
33345
  }
32957
33346
 
32958
33347
  // src/history.ts
@@ -33005,7 +33394,7 @@ function exitForStatus(status) {
33005
33394
  init_define_KT_HUB_FILES();
33006
33395
  var import_child_process2 = require("child_process");
33007
33396
  var import_crypto = require("crypto");
33008
- var import_fs2 = require("fs");
33397
+ var import_fs3 = require("fs");
33009
33398
  var import_os = require("os");
33010
33399
  var import_path2 = require("path");
33011
33400
 
@@ -33103,7 +33492,7 @@ function run(bin, args, store, verb, input) {
33103
33492
  var SECRET_TOOL_DIRS = ["/usr/bin", "/bin", "/usr/local/bin", "/run/current-system/sw/bin"];
33104
33493
  var isExecutable = (p) => {
33105
33494
  try {
33106
- (0, import_fs2.accessSync)(p, import_fs2.constants.X_OK);
33495
+ (0, import_fs3.accessSync)(p, import_fs3.constants.X_OK);
33107
33496
  return true;
33108
33497
  } catch {
33109
33498
  return false;
@@ -33222,7 +33611,7 @@ function why(r, ...secrets) {
33222
33611
  function fileStore(file = FILE) {
33223
33612
  let raw;
33224
33613
  try {
33225
- raw = (0, import_fs2.readFileSync)(file, "utf8");
33614
+ raw = (0, import_fs3.readFileSync)(file, "utf8");
33226
33615
  } catch (e) {
33227
33616
  if (e.code === "ENOENT") return {};
33228
33617
  throw new Error(`cannot read the token file ${file}: ${e.message}`);
@@ -33239,9 +33628,9 @@ function fileStore(file = FILE) {
33239
33628
  var dirLocked = false;
33240
33629
  function ensureFileDir() {
33241
33630
  const dir = (0, import_path2.dirname)(FILE);
33242
- (0, import_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
33631
+ (0, import_fs3.mkdirSync)(dir, { recursive: true, mode: 448 });
33243
33632
  if ((0, import_os.platform)() !== "win32") try {
33244
- (0, import_fs2.chmodSync)(dir, 448);
33633
+ (0, import_fs3.chmodSync)(dir, 448);
33245
33634
  } catch {
33246
33635
  }
33247
33636
  else if (!dirLocked) {
@@ -33270,32 +33659,32 @@ function withLockFile(lock, fn, afterJudge) {
33270
33659
  const deadline = Date.now() + LOCK_WAIT_MS;
33271
33660
  for (; ; ) {
33272
33661
  try {
33273
- const fd = (0, import_fs2.openSync)(lock, "wx", 384);
33662
+ const fd = (0, import_fs3.openSync)(lock, "wx", 384);
33274
33663
  try {
33275
- (0, import_fs2.writeSync)(fd, owner);
33664
+ (0, import_fs3.writeSync)(fd, owner);
33276
33665
  } finally {
33277
- (0, import_fs2.closeSync)(fd);
33666
+ (0, import_fs3.closeSync)(fd);
33278
33667
  }
33279
33668
  break;
33280
33669
  } catch (e) {
33281
33670
  const code = e.code;
33282
33671
  if (code !== "EEXIST" && code !== "EPERM" && code !== "EACCES") throw e;
33283
- if (code !== "EEXIST" && !(0, import_fs2.existsSync)(lock)) throw new Error(`cannot write ${(0, import_path2.dirname)(lock)}: ${e.message}`);
33672
+ if (code !== "EEXIST" && !(0, import_fs3.existsSync)(lock)) throw new Error(`cannot write ${(0, import_path2.dirname)(lock)}: ${e.message}`);
33284
33673
  try {
33285
- if (Date.now() - (0, import_fs2.statSync)(lock).mtimeMs > LOCK_STALE_MS) {
33286
- const judged = (0, import_fs2.readFileSync)(lock, "utf8");
33674
+ if (Date.now() - (0, import_fs3.statSync)(lock).mtimeMs > LOCK_STALE_MS) {
33675
+ const judged = (0, import_fs3.readFileSync)(lock, "utf8");
33287
33676
  afterJudge?.();
33288
33677
  const gone = `${lock}.${process.pid}.${(0, import_crypto.randomBytes)(4).toString("hex")}`;
33289
- (0, import_fs2.renameSync)(lock, gone);
33290
- if ((0, import_fs2.readFileSync)(gone, "utf8") === judged) {
33291
- (0, import_fs2.rmSync)(gone, { force: true });
33678
+ (0, import_fs3.renameSync)(lock, gone);
33679
+ if ((0, import_fs3.readFileSync)(gone, "utf8") === judged) {
33680
+ (0, import_fs3.rmSync)(gone, { force: true });
33292
33681
  continue;
33293
33682
  }
33294
33683
  try {
33295
- (0, import_fs2.linkSync)(gone, lock);
33684
+ (0, import_fs3.linkSync)(gone, lock);
33296
33685
  } catch {
33297
33686
  }
33298
- (0, import_fs2.rmSync)(gone, { force: true });
33687
+ (0, import_fs3.rmSync)(gone, { force: true });
33299
33688
  }
33300
33689
  } catch {
33301
33690
  }
@@ -33307,7 +33696,7 @@ function withLockFile(lock, fn, afterJudge) {
33307
33696
  return fn();
33308
33697
  } finally {
33309
33698
  try {
33310
- if ((0, import_fs2.readFileSync)(lock, "utf8") === owner) (0, import_fs2.rmSync)(lock, { force: true });
33699
+ if ((0, import_fs3.readFileSync)(lock, "utf8") === owner) (0, import_fs3.rmSync)(lock, { force: true });
33311
33700
  } catch {
33312
33701
  }
33313
33702
  }
@@ -33316,10 +33705,10 @@ var withFileLock = (fn) => withLockFile(`${FILE}.lock`, fn);
33316
33705
  function sweepStaleTemps(file) {
33317
33706
  const dir = (0, import_path2.dirname)(file), prefix = `${(0, import_path2.basename)(file)}.`;
33318
33707
  try {
33319
- for (const n of (0, import_fs2.readdirSync)(dir)) {
33708
+ for (const n of (0, import_fs3.readdirSync)(dir)) {
33320
33709
  if (!n.startsWith(prefix) || !n.endsWith(".tmp")) continue;
33321
33710
  try {
33322
- if (Date.now() - (0, import_fs2.statSync)((0, import_path2.join)(dir, n)).mtimeMs > LOCK_STALE_MS) (0, import_fs2.rmSync)((0, import_path2.join)(dir, n), { force: true });
33711
+ if (Date.now() - (0, import_fs3.statSync)((0, import_path2.join)(dir, n)).mtimeMs > LOCK_STALE_MS) (0, import_fs3.rmSync)((0, import_path2.join)(dir, n), { force: true });
33323
33712
  } catch {
33324
33713
  }
33325
33714
  }
@@ -33329,16 +33718,16 @@ function sweepStaleTemps(file) {
33329
33718
  function writeFileStore(store) {
33330
33719
  const tmp = `${FILE}.${process.pid}.${(0, import_crypto.randomBytes)(6).toString("hex")}.tmp`;
33331
33720
  try {
33332
- (0, import_fs2.closeSync)((0, import_fs2.openSync)(tmp, "wx", 384));
33721
+ (0, import_fs3.closeSync)((0, import_fs3.openSync)(tmp, "wx", 384));
33333
33722
  if ((0, import_os.platform)() === "win32") lockDownWin(tmp, "file");
33334
33723
  else try {
33335
- (0, import_fs2.chmodSync)(tmp, 384);
33724
+ (0, import_fs3.chmodSync)(tmp, 384);
33336
33725
  } catch {
33337
33726
  }
33338
- (0, import_fs2.writeFileSync)(tmp, JSON.stringify(store));
33727
+ (0, import_fs3.writeFileSync)(tmp, JSON.stringify(store));
33339
33728
  for (let i = 0; ; i++) {
33340
33729
  try {
33341
- (0, import_fs2.renameSync)(tmp, FILE);
33730
+ (0, import_fs3.renameSync)(tmp, FILE);
33342
33731
  break;
33343
33732
  } catch (e) {
33344
33733
  const code = e.code;
@@ -33347,7 +33736,7 @@ function writeFileStore(store) {
33347
33736
  }
33348
33737
  }
33349
33738
  } catch (e) {
33350
- (0, import_fs2.rmSync)(tmp, { force: true });
33739
+ (0, import_fs3.rmSync)(tmp, { force: true });
33351
33740
  throw e;
33352
33741
  }
33353
33742
  }
@@ -33358,7 +33747,7 @@ function updateFileStore(mutate) {
33358
33747
  const store = fileStore();
33359
33748
  if (!mutate(store)) return;
33360
33749
  if (Object.keys(store).length) writeFileStore(store);
33361
- else (0, import_fs2.rmSync)(FILE, { force: true });
33750
+ else (0, import_fs3.rmSync)(FILE, { force: true });
33362
33751
  });
33363
33752
  }
33364
33753
  function dropFileEntry(base) {
@@ -34980,9 +35369,9 @@ function installLocationFromRegistry(regOutput) {
34980
35369
  }
34981
35370
  function underInstallRoot(dir, roots) {
34982
35371
  if (!import_node_path3.win32.isAbsolute(dir)) return false;
34983
- const norm = (p) => import_node_path3.win32.resolve(p).toLowerCase().replace(/[\\/]+$/, "");
34984
- const d = norm(dir);
34985
- return roots.some((r) => r && import_node_path3.win32.isAbsolute(r) && d.startsWith(norm(r) + "\\"));
35372
+ const norm2 = (p) => import_node_path3.win32.resolve(p).toLowerCase().replace(/[\\/]+$/, "");
35373
+ const d = norm2(dir);
35374
+ return roots.some((r) => r && import_node_path3.win32.isAbsolute(r) && d.startsWith(norm2(r) + "\\"));
34986
35375
  }
34987
35376
  function hubLauncher(env = {}) {
34988
35377
  const platform3 = env.platform ?? process.platform;
@@ -35134,13 +35523,14 @@ var host = () => (0, import_os3.hostname)().trim().replace(/\.local$/, "") || "t
35134
35523
  var ownDeviceName = () => `Kutrion CLI on ${host()}`.slice(0, 255);
35135
35524
  async function registerOwnDevice(deps) {
35136
35525
  const tty = deps.tty ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
35137
- if (seedSkipReason(deps.env ?? process.env, deps.platform ?? process.platform, tty)) return { kind: "skipped", note: null };
35526
+ const previous = deps.previousDeviceId ?? null;
35527
+ if (!previous && seedSkipReason(deps.env ?? process.env, deps.platform ?? process.platform, tty)) return { kind: "skipped", note: null };
35138
35528
  if (!deps.bearer || !deps.keychainAvailable()) return { kind: "skipped", note: null };
35139
35529
  const { store, failedWrite } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
35140
35530
  const key = resolveDeviceKey(store);
35141
35531
  const res = await deps.post(
35142
35532
  "/auth/devices/register",
35143
- { name: ownDeviceName(), platform: "desktop", client: "cli", public_key: key.publicSpkiB64 },
35533
+ { name: ownDeviceName(), platform: "desktop", client: "cli", public_key: key.publicSpkiB64, ...previous ? { previous_device_id: previous } : {} },
35144
35534
  deps.bearer
35145
35535
  );
35146
35536
  if (!res.ok) {
@@ -35310,16 +35700,16 @@ function redactProxyUrls(text2) {
35310
35700
  function bypassesProxy(host2, port, no) {
35311
35701
  if (!no) return false;
35312
35702
  for (const raw of no.split(/[\s,]+/)) {
35313
- let entry = raw.trim().toLowerCase();
35314
- if (!entry) continue;
35315
- if (entry === "*") return true;
35316
- const colon = entry.match(/^(.*):(\d+)$/);
35703
+ let entry2 = raw.trim().toLowerCase();
35704
+ if (!entry2) continue;
35705
+ if (entry2 === "*") return true;
35706
+ const colon = entry2.match(/^(.*):(\d+)$/);
35317
35707
  if (colon) {
35318
35708
  if (colon[2] !== port) continue;
35319
- entry = colon[1];
35709
+ entry2 = colon[1];
35320
35710
  }
35321
- entry = entry.replace(/^\*?\./, "");
35322
- if (host2 === entry || host2.endsWith(`.${entry}`)) return true;
35711
+ entry2 = entry2.replace(/^\*?\./, "");
35712
+ if (host2 === entry2 || host2.endsWith(`.${entry2}`)) return true;
35323
35713
  }
35324
35714
  return false;
35325
35715
  }
@@ -35473,8 +35863,8 @@ function firstBindingWarning(name, group, host2, port) {
35473
35863
  ...group.protocols.length > 1 ? ["", "Authorizing SSH also authorizes SFTP. They are one destination."] : []
35474
35864
  ];
35475
35865
  }
35476
- function removalWarning(name, group, entry, remainingAfter, sameGroupRemaining) {
35477
- const pairNote = entry.rows.length > 1 ? ["", "Both the SSH and the SFTP authorization for this host are removed."] : [];
35866
+ function removalWarning(name, group, entry2, remainingAfter, sameGroupRemaining) {
35867
+ const pairNote = entry2.rows.length > 1 ? ["", "Both the SSH and the SFTP authorization for this host are removed."] : [];
35478
35868
  if (remainingAfter === 0) {
35479
35869
  return [
35480
35870
  `This is the only destination authorized on "${name}".`,
@@ -35493,7 +35883,7 @@ function removalWarning(name, group, entry, remainingAfter, sameGroupRemaining)
35493
35883
  ];
35494
35884
  }
35495
35885
  return [
35496
- `"${name}" will no longer be accepted at ${entry.host}:${entry.port} over ${group.protocols.map((p) => PROTOCOL_LABEL[p]).join(" or ")}.`,
35886
+ `"${name}" will no longer be accepted at ${entry2.host}:${entry2.port} over ${group.protocols.map((p) => PROTOCOL_LABEL[p]).join(" or ")}.`,
35497
35887
  "",
35498
35888
  `Its other ${group.label} destinations are unchanged.`,
35499
35889
  ...pairNote
@@ -35664,16 +36054,22 @@ var REFUSALS = {
35664
36054
  why: "A jump host must be one of your own SSH connections, and no connection of yours has that id.",
35665
36055
  action: "Run `kt conns` and pass an id from the first column to --jump."
35666
36056
  },
35667
- // auth-guard.ts:109 (social-session gate) and :167 (adaptive), plus rotation.ts:159, jit.ts:226,
35668
- // fleet.ts:190, agents.ts:146, modules/ssh.ts:730 and :1228. The remedy is mechanical and worth
35669
- // stating exactly, because it is not obvious that it can be satisfied anywhere but here: step-up
35670
- // freshness is a per-USER Redis proof with a ~5 minute TTL (server/src/lib/step-up.ts:16), not a
35671
- // claim baked into this CLI's token. Stepping up in the web app on the same account therefore
35672
- // does clear it here.
36057
+ // #3528: modules/ssh.ts sftpFail and modules/files.ts send this for a remote path, not a record id, so
36058
+ // the 404 line about an invisible id would point at the wrong thing.
36059
+ path_not_found: {
36060
+ title: "No such path",
36061
+ why: "The connection was reached, but nothing exists at that path on the remote side.",
36062
+ action: "Check the path. `kt sftp ls <connId>` with no path lists the connection's starting directory."
36063
+ },
36064
+ // lib/auth-guard.ts blockStepUpPending (social-session gate) and enforceAdaptiveAuth (adaptive), plus
36065
+ // the rotation, jit, fleet, agents and ssh modules. Step-up freshness is a per-SESSION Redis proof with
36066
+ // a short TTL (server/src/lib/step-up.ts), so a step-up in the web app does not clear this CLI's
36067
+ // session; `kt step-up` proves it for this one. The refusal does not say which factors the account
36068
+ // holds, so the action names a completable path for each case (#3539).
35673
36069
  step_up_required: {
35674
36070
  title: "Step-up verification required",
35675
36071
  why: "This action needs a recent passkey or app-MFA check, and this session does not have one.",
35676
- action: "Only `kt mfa` and `kt passkey rm` ask for an authenticator code here, and only on an account that has one. Otherwise verify a passkey or app-MFA in the Kutrion web app on the same account, then re-run this command within a few minutes. If you signed in through SSO or a social provider, `kt login --email <e> --password <p> --code <2fa>` also mints a session that is not gated."
36072
+ action: "Run `kt step-up` and enter a code from your authenticator app, then re-run this command within a few minutes. With no authenticator app on the account (none yet, or a passkey only, which a terminal cannot verify), enrol one first with `kt mfa setup` and `kt mfa verify --code <code>`, then run `kt step-up`. If you signed in through SSO or a social provider, `kt login --email <e> --password <p> --code <2fa>` also mints a session that is not gated."
35677
36073
  },
35678
36074
  // #2225 AC5, and the one shape this table exists for: the server owns the REASON, this client adds
35679
36075
  // the ROUTE. The server's hint names what ended the session (server/src/index.ts for a credential
@@ -35709,7 +36105,7 @@ var REFUSALS = {
35709
36105
  },
35710
36106
  refresh_hub_revoked: {
35711
36107
  title: "This session was signed out",
35712
- why: refreshWhy("refresh_hub_revoked"),
36108
+ why: refreshWhy("refresh_hub_revoked", "app"),
35713
36109
  action: "Run `kt login` to sign in again."
35714
36110
  },
35715
36111
  device_revoked: {
@@ -35818,7 +36214,7 @@ var REFUSALS = {
35818
36214
  why: "Promoting now would leave those hosts refusing every session, because they do not yet accept a certificate from the next CA.",
35819
36215
  action: "Install or verify each host listed by `kt broker-ca status`, or revoke its enrollment, then promote again."
35820
36216
  },
35821
- // authz.ts:69, connection-import.ts:142. Overage freeze after a downgrade — read stays, write stops.
36217
+ // authz.ts:69, connection-import.ts:164. Overage freeze after a downgrade — read stays, write stops.
35822
36218
  vault_frozen: {
35823
36219
  title: "Vault is read-only",
35824
36220
  why: "This vault is frozen for writes because the plan's vault limit was exceeded after a downgrade.",
@@ -35844,7 +36240,7 @@ var REFUSALS = {
35844
36240
  why: "This organization keeps AI on its own network, so only agents declared `lan` may act for it.",
35845
36241
  action: 'Mint with `--egress lan` only for a model the organization hosts on its own network, or ask an owner to turn off "Keep AI on our network".'
35846
36242
  },
35847
- // files.ts:196.
36243
+ // server/src/modules/files.ts:353.
35848
36244
  tls_downgrade_refused: {
35849
36245
  title: "Transport security cannot be weakened",
35850
36246
  why: "The change would turn TLS off, or allow an unverified certificate, on a connection that already has it on.",
@@ -35900,7 +36296,7 @@ var REFUSALS = {
35900
36296
  why: "The code was valid for a limited time and that time has passed.",
35901
36297
  action: "Run `kt login` again; a new code is emailed when the password is accepted."
35902
36298
  },
35903
- // vault.ts:1105/1363/1387, files.ts:129, shares.ts:43, file-broker.ts:99. #954: the wire field is
36299
+ // vault.ts:1105/1363/1387, server/src/modules/files.ts:369/574, shares.ts:43, file-broker.ts:99. #954: the wire field is
35904
36300
  // `inject_only`; the name the product uses is "No-reveal", and the second half is stated positively
35905
36301
  // so the credential does not read as broken.
35906
36302
  inject_only: {
@@ -35908,12 +36304,42 @@ var REFUSALS = {
35908
36304
  why: "Kutrion does not display, copy, export or share this credential's value. It is still used to authenticate connections and integrations.",
35909
36305
  action: "Use the credential rather than reading it \u2014 `kt ssh <connId>` or `kt exec <connId> <command>` broker a session with it without the value passing through this terminal."
35910
36306
  },
35911
- // modules/ssh.ts:473, modules/rdp.ts:151.
36307
+ // modules/ssh.ts:478, modules/rdp.ts:151.
35912
36308
  inject_only_requires_write: {
35913
36309
  title: "Write access required for a No-reveal credential",
35914
36310
  why: "Pointing a connection at a No-reveal credential needs write access to the vault holding it, because the person doing it decides where the credential gets presented.",
35915
36311
  action: "Ask an owner or editor of that vault to make the change, or to grant you write access to it."
35916
36312
  },
36313
+ // lib/untrusted-cert.ts, sent by files.ts fail() and cloud.ts fail() as a 503 (#3418); admitted past the 5xx
36314
+ // gate by SERVER_REFUSALS. Wording matches Kutrion Transfer's entry.
36315
+ // lib/remote-auth.ts, sent by files.ts fail() and ssh.ts sftpFail() as a 422 (#3451). Wording shared with Kutrion Transfer.
36316
+ remote_auth_failed: {
36317
+ title: "The server rejected the sign-in",
36318
+ why: REMOTE_AUTH_FAILED_TEXT,
36319
+ action: "Update the credential this connection uses in the vault, then retry."
36320
+ },
36321
+ // lib/remote-auth.ts, sent by files.ts fail() as a 400 (#3462). Nothing was sent to the host. Wording shared with Kutrion Transfer.
36322
+ basic_over_http_refused: {
36323
+ title: "The password was not sent over plain HTTP",
36324
+ why: BASIC_OVER_HTTP_REFUSED_TEXT,
36325
+ action: "Switch this connection to HTTPS, then retry."
36326
+ },
36327
+ basic_after_digest_refused: {
36328
+ title: "The server's sign-in method changed",
36329
+ why: BASIC_AFTER_DIGEST_REFUSED_TEXT,
36330
+ action: "Kutrion refused because someone between you and the server may be removing the safer sign-in option. Ask whoever runs the server whether they changed its sign-in settings. If the change is genuine, delete this connection and add it again, or, if you own it, edit it to the server's new address (this needs step-up). Only adding it again resets the remembered sign-in type for the same address."
36331
+ },
36332
+ // lib/remote-auth.ts, sent by files.ts fail() as a 400 (#3484). Nothing was sent to the host.
36333
+ credential_control_characters: {
36334
+ title: "The saved sign-in cannot be sent",
36335
+ why: CREDENTIAL_CONTROL_CHARS_TEXT,
36336
+ action: CREDENTIAL_CONTROL_CHARS_NEXT
36337
+ },
36338
+ tls_certificate_untrusted: {
36339
+ title: "This host's certificate is not trusted",
36340
+ why: "The host presented a certificate that no authority vouches for, often a self-signed one, so the connection was not made. Nothing was sent to the host.",
36341
+ action: "Ask an admin to review the certificate or the network path to this host."
36342
+ },
35917
36343
  // access-policy.ts:264, reached from a root preHandler over the file family; reason values from AccessDecision.reason. The server sends
35918
36344
  // a bare code here and no hint, so translating `reason` is the whole value this entry adds.
35919
36345
  blocked_by_access_policy: {
@@ -35921,7 +36347,7 @@ var REFUSALS = {
35921
36347
  why: "This organization restricts where its file connections may be reached from, and this request came from outside that.",
35922
36348
  action: "Connect from an allowed network or country, or ask an organization administrator to widen the policy. The `reason` below says which rule refused it."
35923
36349
  },
35924
- // files.ts:34/795/889, file-requests.ts:280. `path` and `rule` name the file and the rule.
36350
+ // server/src/modules/files.ts:56/1054, file-requests.ts:280. `path` and `rule` name the file and the rule.
35925
36351
  blocked_by_dlp_policy: {
35926
36352
  title: "Blocked by a data-loss-prevention rule",
35927
36353
  why: "The content or the path matched a rule this organization applies to file transfers.",
@@ -36091,8 +36517,14 @@ function contextLines(body, indent) {
36091
36517
  const text2 = Array.isArray(v) ? v.filter((x) => x !== null && typeof x !== "object").map((x) => String(x)).join(", ") : String(v);
36092
36518
  if (text2.trim()) out.push(`${indent}${f}: ${text2}`);
36093
36519
  }
36520
+ const fields = asRecord(asRecord(b.details)?.fieldErrors);
36521
+ for (const [field2, msgs] of Object.entries(fields ?? {})) {
36522
+ const text2 = Array.isArray(msgs) ? msgs.filter((m) => typeof m === "string").map(scrubInput).join("; ") : "";
36523
+ if (text2) out.push(`${indent}${field2}: ${text2}`);
36524
+ }
36094
36525
  return out;
36095
36526
  }
36527
+ var scrubInput = (m) => m.replace(/,?\s*(?:received|got)\s+(?:'[^']*'|"[^"]*"|\S+)/gi, "").replace(/\s{2,}/g, " ").replace(/[\s,]+$/, "").trim();
36096
36528
  var STATUS_ACTION = {
36097
36529
  400: "The server could not use the request as sent. Check the command's arguments against `kt --help`.",
36098
36530
  401: "This machine's session was not accepted. Run `kt login` to sign in again.",
@@ -36109,7 +36541,7 @@ var HUB_SIGN_IN_ACTION = {
36109
36541
  status: "This machine's session was not accepted. Run `kt login`, it signs in through Kutrion Hub.",
36110
36542
  signed_out: "Run `kt login`, it signs in through Kutrion Hub."
36111
36543
  };
36112
- var SERVER_REFUSALS = /* @__PURE__ */ new Set(["payment_unconfirmed"]);
36544
+ var SERVER_REFUSALS = /* @__PURE__ */ new Set(["payment_unconfirmed", "tls_certificate_untrusted"]);
36113
36545
  var sentenceKey = (t) => t.toLowerCase().replace(/[^a-z0-9]+/g, "");
36114
36546
  var sameSentence = (a, b) => b !== void 0 && sentenceKey(a) === sentenceKey(b);
36115
36547
  var SIGNIN_CODE_REFUSALS = /* @__PURE__ */ new Set(["email_code_required", "signin_code_cap"]);
@@ -36140,6 +36572,10 @@ function refusalLines(e, indent = " ", opts2 = {}) {
36140
36572
  const code = fromRefresh ? e.refreshCode : refusalCode(e);
36141
36573
  const platformFlag = platformFlagRefusalText(code);
36142
36574
  if (platformFlag) return [`Turned off on this deployment (${code}, HTTP ${e.status})`, "", platformFlag];
36575
+ const privateHost = code === "blocked_internal_address" ? str6(asRecord(e.body)?.host) : null;
36576
+ if (privateHost) {
36577
+ return [`Refused by the server (${code}, HTTP ${e.status})`, "", privateTargetSentence({ host: privateHost, surface: "short", reason: asRecord(e.body)?.target_reason })];
36578
+ }
36143
36579
  const copy = Object.hasOwn(REFUSALS, code) ? REFUSALS[code] : void 0;
36144
36580
  const server = fromRefresh || SIGNIN_CODE_REFUSALS.has(code) ? null : serverText(e.body);
36145
36581
  const context = contextLines(e.body, indent);
@@ -36214,7 +36650,9 @@ function wsRefusalLines(frame, indent = " ", jumpCtx = {}) {
36214
36650
  const jump = jumpRefusalReport(frame, code, head, jumpCtx, indent);
36215
36651
  if (jump) return jump;
36216
36652
  const lines = [code && !ourFault ? `${head} (${code})` : head, ""];
36217
- lines.push(str6(frame.error) ?? "The server sent no explanation with this refusal.");
36653
+ lines.push(
36654
+ code === "blocked_internal_address" && host2 ? privateTargetSentence({ host: host2, surface: "short", reason: d.target_reason }) : str6(frame.error) ?? "The server sent no explanation with this refusal."
36655
+ );
36218
36656
  if (host2) lines.push("", `${indent}endpoint: ${port === null ? host2 : `${host2}:${port}`}`);
36219
36657
  if (allowed.length) {
36220
36658
  lines.push("", destinationRefusalAllowedLabel(dest));
@@ -36227,6 +36665,17 @@ function wsRefusalLines(frame, indent = " ", jumpCtx = {}) {
36227
36665
  }
36228
36666
 
36229
36667
  // src/shell.ts
36668
+ function ttySize(columns, rows) {
36669
+ const known = (n, d) => typeof n === "number" && Number.isFinite(n) && n > 0 ? n : d;
36670
+ return [known(columns, 80), known(rows, 24)];
36671
+ }
36672
+ function detachNotice(frame) {
36673
+ const ms = frame.orphan_ttl_ms;
36674
+ const left = typeof ms === "number" && ms > 0 ? ms >= 6e4 ? `${Math.round(ms / 6e4)} minute${Math.round(ms / 6e4) === 1 ? "" : "s"}` : `${Math.ceil(ms / 1e3)} seconds` : null;
36675
+ const held = left ? `The session keeps running on the server for ${left}, then ends.` : "The session keeps running on the server for a short time, then ends.";
36676
+ if (frame.recorded !== true) return `[Kutrion] Detached. ${held}`;
36677
+ return `[Kutrion] Detached. ${held} Its recording is saved when the session ends, and kt recordings lists it from then. To end a session at once, type exit in it instead of detaching.`;
36678
+ }
36230
36679
  function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onChainedHostKey) {
36231
36680
  const wsUrl = shellUrl(base, connId);
36232
36681
  const ws = new wrapper_default(wsUrl, { headers: { Authorization: `Bearer ${token}` }, agent: wsAgentFor(wsUrl) });
@@ -36235,12 +36684,12 @@ function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onCha
36235
36684
  const isTty = Boolean(stdin.isTTY && stdout.isTTY);
36236
36685
  const sendResize = () => {
36237
36686
  if (ws.readyState !== wrapper_default.OPEN) return;
36238
- const cols = stdout.columns ?? 80;
36239
- const rows = stdout.rows ?? 24;
36687
+ const [cols, rows] = ttySize(stdout.columns, stdout.rows);
36240
36688
  ws.send(resizeFrame(cols, rows));
36241
36689
  };
36242
36690
  let exiting = false;
36243
36691
  let recordedSessionId = null;
36692
+ let sessionInfo = null;
36244
36693
  const printReceipt = async () => {
36245
36694
  const id = recordedSessionId;
36246
36695
  if (!id) return;
@@ -36262,6 +36711,9 @@ function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onCha
36262
36711
  ws.close();
36263
36712
  } catch {
36264
36713
  }
36714
+ if (!ended && sessionInfo) process.stderr.write(`\r
36715
+ ${detachNotice(sessionInfo)}\r
36716
+ `);
36265
36717
  void (ended ? printReceipt() : Promise.resolve()).catch(() => {
36266
36718
  }).then(() => onExit?.()).catch(() => {
36267
36719
  }).then(() => process.exit(code));
@@ -36302,6 +36754,7 @@ ${wsRefusalLines(frame, " ", jumpCtx).join("\r\n")}\r
36302
36754
  const sessionFrame = sawSession ? null : parseSessionFrame(text2);
36303
36755
  if (sessionFrame) {
36304
36756
  sawSession = true;
36757
+ sessionInfo = sessionFrame;
36305
36758
  const state = recordingStateLabel(sessionFrame);
36306
36759
  if (state) process.stderr.write(`\r
36307
36760
  [Kutrion] ${state}\r
@@ -36379,7 +36832,7 @@ var import_readline = require("readline");
36379
36832
 
36380
36833
  // src/mcp-install.ts
36381
36834
  init_define_KT_HUB_FILES();
36382
- var import_fs3 = require("fs");
36835
+ var import_fs4 = require("fs");
36383
36836
  var import_os4 = require("os");
36384
36837
  var import_path4 = require("path");
36385
36838
  var import_child_process3 = require("child_process");
@@ -36474,35 +36927,35 @@ function bundleCandidates(moduleDir) {
36474
36927
  function resolveLaunch(base, client, moduleDir = __dirname) {
36475
36928
  const which = (0, import_child_process3.spawnSync)("command -v kt", { encoding: "utf8", shell: true });
36476
36929
  const onPath = which.status === 0 ? (which.stdout ?? "").trim().split("\n")[0] : "";
36477
- let entry;
36930
+ let entry2;
36478
36931
  if ((0, import_path4.isAbsolute)(onPath)) {
36479
- entry = { command: onPath, args: ["mcp"] };
36932
+ entry2 = { command: onPath, args: ["mcp"] };
36480
36933
  } else {
36481
- const bundle = bundleCandidates(moduleDir).find((p) => (0, import_fs3.existsSync)(p));
36934
+ const bundle = bundleCandidates(moduleDir).find((p) => (0, import_fs4.existsSync)(p));
36482
36935
  if (!bundle) {
36483
36936
  throw new Error(
36484
36937
  "no built CLI to launch the MCP bridge with. Run `npm run build` in cli/, or install the CLI so that `kt` is on your PATH, then re-run this command."
36485
36938
  );
36486
36939
  }
36487
- entry = { command: process.execPath, args: [bundle, "mcp"] };
36940
+ entry2 = { command: process.execPath, args: [bundle, "mcp"] };
36488
36941
  }
36489
36942
  const fileStore2 = (ktEnv("TOKEN_STORE") ?? "").trim().toLowerCase() === "file";
36490
- entry.env = {
36943
+ entry2.env = {
36491
36944
  KT_API_BASE: base,
36492
36945
  ...client ? { KT_MCP_CLIENT: client } : {},
36493
36946
  ...fileStore2 ? { KT_TOKEN_STORE: "file" } : {}
36494
36947
  };
36495
- return entry;
36948
+ return entry2;
36496
36949
  }
36497
36950
  var STORE_ENV = /* @__PURE__ */ new Set(["TOKEN_STORE", "TOKEN_STORE_TIMEOUT_MS"]);
36498
- async function verifyLaunch(entry, timeoutMs = 15e3) {
36951
+ async function verifyLaunch(entry2, timeoutMs = 15e3) {
36499
36952
  const probe = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
36500
36953
  const inherited = Object.fromEntries(
36501
36954
  Object.entries(process.env).filter(([k]) => !/^(KT|VT)_/.test(k) || STORE_ENV.has(k.slice(3)))
36502
36955
  );
36503
- const res = (0, import_child_process3.spawnSync)(entry.command, entry.args, {
36956
+ const res = (0, import_child_process3.spawnSync)(entry2.command, entry2.args, {
36504
36957
  input: probe + "\n",
36505
- env: { ...inherited, ...entry.env ?? {} },
36958
+ env: { ...inherited, ...entry2.env ?? {} },
36506
36959
  cwd: (0, import_os4.tmpdir)(),
36507
36960
  encoding: "utf8",
36508
36961
  timeout: timeoutMs
@@ -36521,10 +36974,10 @@ async function verifyLaunch(entry, timeoutMs = 15e3) {
36521
36974
  return { ok: false, error: "the bridge produced output that is not JSON-RPC" };
36522
36975
  }
36523
36976
  }
36524
- function mergeConfig(existing, key, entry) {
36977
+ function mergeConfig(existing, key, entry2) {
36525
36978
  const cfg = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
36526
36979
  const servers = cfg[key] && typeof cfg[key] === "object" && !Array.isArray(cfg[key]) ? { ...cfg[key] } : {};
36527
- servers[SERVER_NAME] = entry;
36980
+ servers[SERVER_NAME] = entry2;
36528
36981
  cfg[key] = servers;
36529
36982
  return cfg;
36530
36983
  }
@@ -36540,14 +36993,14 @@ function removeFromConfig(existing, key) {
36540
36993
  }
36541
36994
  function readJson(path) {
36542
36995
  try {
36543
- return JSON.parse((0, import_fs3.readFileSync)(path, "utf8"));
36996
+ return JSON.parse((0, import_fs4.readFileSync)(path, "utf8"));
36544
36997
  } catch {
36545
36998
  return void 0;
36546
36999
  }
36547
37000
  }
36548
37001
  function writeJson(path, value) {
36549
- (0, import_fs3.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
36550
- (0, import_fs3.writeFileSync)(path, JSON.stringify(value, null, 2) + "\n");
37002
+ (0, import_fs4.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
37003
+ (0, import_fs4.writeFileSync)(path, JSON.stringify(value, null, 2) + "\n");
36551
37004
  }
36552
37005
  var agentAccount = (base, client) => client ? `agent:${base}:${client}` : `agent:${base}`;
36553
37006
  function getAgentToken(base, client) {
@@ -36592,7 +37045,7 @@ async function installClient(o) {
36592
37045
  if (!configPath) {
36593
37046
  throw new Error(`${target.label} is not available on this platform (${(0, import_os4.platform)()}).`);
36594
37047
  }
36595
- const entry = resolveLaunch(o.base, o.client);
37048
+ const entry2 = resolveLaunch(o.base, o.client);
36596
37049
  const mint = () => api2(o.base, "/agents", o.userToken, {
36597
37050
  method: "POST",
36598
37051
  body: JSON.stringify({
@@ -36601,7 +37054,8 @@ async function installClient(o) {
36601
37054
  client: o.client,
36602
37055
  scopes: o.scopes,
36603
37056
  ttl_minutes: o.ttlDays * 24 * 60,
36604
- egress_class: o.egressClass ?? "cloud"
37057
+ egress_class: o.egressClass ?? "cloud",
37058
+ ...o.resourceScope ? { resource_scope: o.resourceScope } : {}
36605
37059
  })
36606
37060
  });
36607
37061
  let superseded = 0;
@@ -36642,16 +37096,17 @@ async function installClient(o) {
36642
37096
  superseded += old.revoked;
36643
37097
  revokeFailed.push(...old.revokeFailed);
36644
37098
  agentListStatus ??= old.agentListStatus;
36645
- writeJson(configPath, mergeConfig(readJson(configPath), target.key, entry));
37099
+ writeJson(configPath, mergeConfig(readJson(configPath), target.key, entry2));
36646
37100
  const who = await api2(o.base, "/mcp/whoami", agent.token);
36647
37101
  return {
37102
+ ...o.resourceScope ? { resourceScope: o.resourceScope } : {},
36648
37103
  configPath,
36649
37104
  principalId: agent.id,
36650
37105
  expiresAt: agent.expires_at,
36651
37106
  scopes: agent.scopes,
36652
37107
  next: target.next,
36653
37108
  verified: who.status === 200 && who.body?.principal_id === agent.id,
36654
- launch: await verifyLaunch(entry),
37109
+ launch: await verifyLaunch(entry2),
36655
37110
  superseded,
36656
37111
  revokeFailed,
36657
37112
  ...agentListStatus !== void 0 ? { agentListStatus } : {}
@@ -36669,7 +37124,7 @@ async function uninstallClient(o) {
36669
37124
  if (o.client) {
36670
37125
  const target = CLIENTS[o.client];
36671
37126
  configPath = target.path(o.dir);
36672
- if (configPath && (0, import_fs3.existsSync)(configPath)) {
37127
+ if (configPath && (0, import_fs4.existsSync)(configPath)) {
36673
37128
  writeJson(configPath, removeFromConfig(readJson(configPath), target.key));
36674
37129
  }
36675
37130
  }
@@ -36691,6 +37146,7 @@ async function agentStatus(base, token) {
36691
37146
  principal_id: who.body?.principal_id,
36692
37147
  expires_at: who.body?.expires_at,
36693
37148
  scopes: who.body?.scopes,
37149
+ resource_scope: who.body?.resource_scope,
36694
37150
  rate_limit: who.body?.rate_limit
36695
37151
  };
36696
37152
  }
@@ -36706,17 +37162,66 @@ async function mcpStatus(base, dir) {
36706
37162
  const p = t.path(dir);
36707
37163
  let installed = false;
36708
37164
  let configuredBase = null;
36709
- if (p && (0, import_fs3.existsSync)(p)) {
37165
+ if (p && (0, import_fs4.existsSync)(p)) {
36710
37166
  const cfg = readJson(p);
36711
- const entry = cfg?.[t.key]?.[SERVER_NAME];
36712
- installed = !!entry;
36713
- if (typeof entry?.env?.KT_API_BASE === "string") configuredBase = entry.env.KT_API_BASE;
37167
+ const entry2 = cfg?.[t.key]?.[SERVER_NAME];
37168
+ installed = !!entry2;
37169
+ if (typeof entry2?.env?.KT_API_BASE === "string") configuredBase = entry2.env.KT_API_BASE;
36714
37170
  }
36715
37171
  return { client: id, label: t.label, path: p, installed, configuredBase, agent: await agentStatus(base, getToken(agentAccount(base, id))) };
36716
37172
  }));
36717
37173
  const legacy = await agentStatus(base, getToken(agentAccount(base)));
36718
37174
  return { keychain: backendName(), hasToken: !!legacy || configs.some((c) => c.agent), configs, legacy };
36719
37175
  }
37176
+ function pick2(kind, wanted, options, problems) {
37177
+ const ids = [];
37178
+ for (const w of wanted) {
37179
+ const byId = options.find((o) => o.id === w);
37180
+ const byName = options.filter((o) => o.name === w);
37181
+ if (byId) ids.push(byId.id);
37182
+ else if (byName.length === 1) ids.push(byName[0].id);
37183
+ else if (byName.length > 1) problems.push(`${kind} name "${w}" matches ${byName.length} ${kind}s; use an id (${byName.map((o) => o.id).join(", ")})`);
37184
+ else problems.push(`unknown ${kind}: ${w}`);
37185
+ }
37186
+ return [...new Set(ids)];
37187
+ }
37188
+ async function resolveResourceScope(base, userToken, vaults, connections) {
37189
+ if (!vaults.length && !connections.length) return void 0;
37190
+ const r = await api2(base, "/agents/scope-options", userToken);
37191
+ if (r.status !== 200 || !r.body) throw new Error(`could not list the vaults and connections this account can reach (${r.status})`);
37192
+ const problems = [];
37193
+ const vault_ids = pick2("vault", vaults, r.body.vaults ?? [], problems);
37194
+ const connection_ids = pick2("connection", connections, r.body.connections ?? [], problems);
37195
+ if (problems.length) throw new Error(`${problems.join("; ")}. Nothing was minted.`);
37196
+ return { ...vault_ids.length ? { vault_ids } : {}, ...connection_ids.length ? { connection_ids } : {} };
37197
+ }
37198
+ function describeScope(s) {
37199
+ const v = s?.vault_ids?.length ?? 0;
37200
+ const c = s?.connection_ids?.length ?? 0;
37201
+ const parts = [
37202
+ v ? `${v} vault(s): ${s.vault_ids.join(", ")}` : "all vaults its owner can reach",
37203
+ c ? `${c} connection(s): ${s.connection_ids.join(", ")}` : "all connections its owner can reach"
37204
+ ];
37205
+ return parts.join("; ");
37206
+ }
37207
+ async function mintAgent(o) {
37208
+ const m = await api2(o.base, "/agents", o.userToken, {
37209
+ method: "POST",
37210
+ body: JSON.stringify({
37211
+ name: o.name,
37212
+ purpose: o.purpose,
37213
+ scopes: o.scopes,
37214
+ ttl_minutes: o.ttlDays * 24 * 60,
37215
+ egress_class: o.egressClass,
37216
+ ...o.resourceScope ? { resource_scope: o.resourceScope } : {}
37217
+ })
37218
+ });
37219
+ if (m.status === 402) throw new Error(`your plan does not allow another AI agent (${m.body?.feature ?? "agent_access"}). Revoke one or upgrade.`);
37220
+ if (m.status === 403 && m.body?.error === "on_network_required") throw new Error("this organization requires AI agents to run on its own network. Re-run with --egress lan if the model is self-hosted.");
37221
+ if (m.status !== 201) throw new Error(`could not mint an agent principal (${m.status}): ${JSON.stringify(m.body)}`);
37222
+ if (!m.body?.token || !m.body.id) throw new ApiUnavailableError(m.status, m.body);
37223
+ return { token: m.body.token, id: m.body.id, expires_at: m.body.expires_at, scopes: m.body.scopes };
37224
+ }
36720
37225
 
36721
37226
  // src/mcp.ts
36722
37227
  var MODERN = "2026-07-28";
@@ -37474,9 +37979,23 @@ function recoveryText(rec, s) {
37474
37979
  "A payment may still be due and your access may still be at risk \u2014 this is a display failure, not a resolved account."
37475
37980
  ].join("\n");
37476
37981
  }
37982
+ function modeString(mode, type) {
37983
+ const lead = type === "dir" ? "d" : type === "link" ? "l" : "-";
37984
+ return lead + [6, 3, 0].map((shift) => {
37985
+ const bits = mode >> shift & 7;
37986
+ return (bits & 4 ? "r" : "-") + (bits & 2 ? "w" : "-") + (bits & 1 ? "x" : "-");
37987
+ }).join("");
37988
+ }
37989
+ function whoamiLines(user) {
37990
+ const org = user.org_name ? `${user.org_name}${user.org_role ? ` (${user.org_role})` : ""}` : user.org_role ?? "none";
37991
+ return `email ${user.email}
37992
+ org ${org}`;
37993
+ }
37477
37994
 
37478
37995
  // src/private-vault.ts
37479
37996
  init_define_KT_HUB_FILES();
37997
+ var import_fs5 = require("fs");
37998
+ var import_path5 = require("path");
37480
37999
  var PASSPHRASE_STDIN_FLAG = "vault-passphrase-stdin";
37481
38000
  var PrivateVaultError = class extends Error {
37482
38001
  };
@@ -37494,6 +38013,9 @@ There is no flag that takes the passphrase itself: a flag lands in shell history
37494
38013
  return io.ask("vault passphrase: ");
37495
38014
  }
37496
38015
  async function unlockForCommand(client, io) {
38016
+ return (await unlockVault(client, io)).key;
38017
+ }
38018
+ async function unlockVault(client, io) {
37497
38019
  const passphrase = await readVaultPassphrase(io);
37498
38020
  if (!passphrase) throw new PrivateVaultError("no vault passphrase was given");
37499
38021
  let record;
@@ -37506,7 +38028,7 @@ async function unlockForCommand(client, io) {
37506
38028
  throw e;
37507
38029
  }
37508
38030
  try {
37509
- return await unlockVaultKey(record, passphrase);
38031
+ return { key: await unlockVaultKey(record, passphrase), enrolledAt: record.created_at };
37510
38032
  } catch (e) {
37511
38033
  if (e instanceof VaultKeyError && e.code === "wrong_secret") throw new PrivateVaultError(PRIVATE_VAULT_COPY.wrongPassphrase);
37512
38034
  throw e;
@@ -37517,12 +38039,115 @@ async function openSealedItem(client, key, vaultId, credId) {
37517
38039
  if (!sealed) throw new PrivateVaultError("this item is No-reveal. Kutrion does not display, copy or export its value; it is still used to authenticate connections and integrations.");
37518
38040
  try {
37519
38041
  const opened = await openPrivateItem({ id: credential.id, vault_id: credential.vault_id }, sealed, key);
37520
- return { value: opened.value };
38042
+ return { value: opened.value, notes: opened.notes };
37521
38043
  } catch (e) {
37522
38044
  if (e instanceof VaultKeyError) throw new PrivateVaultError("this item could not be opened with your vault key.");
37523
38045
  throw e;
37524
38046
  }
37525
38047
  }
38048
+ async function probeVaultCustody(client, vaultId) {
38049
+ try {
38050
+ return { private: true, key: (await client.getPrivateDataKey(vaultId)).key };
38051
+ } catch (e) {
38052
+ if (e instanceof ApiError && e.status === 409 && e.slug === "vault_not_private") return { private: false };
38053
+ throw e;
38054
+ }
38055
+ }
38056
+ var PrivateVaultMemory = class {
38057
+ constructor(file) {
38058
+ this.file = file;
38059
+ }
38060
+ read() {
38061
+ try {
38062
+ const parsed = JSON.parse((0, import_fs5.readFileSync)(this.file, "utf8"));
38063
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
38064
+ } catch {
38065
+ return {};
38066
+ }
38067
+ }
38068
+ seen(base, vaultId) {
38069
+ const ids = this.read()[base];
38070
+ return Array.isArray(ids) && ids.includes(vaultId);
38071
+ }
38072
+ remember(base, vaultId) {
38073
+ const all = this.read();
38074
+ const ids = Array.isArray(all[base]) ? all[base] : [];
38075
+ if (ids.includes(vaultId)) return;
38076
+ all[base] = [...ids, vaultId];
38077
+ (0, import_fs5.mkdirSync)((0, import_path5.dirname)(this.file), { recursive: true, mode: 448 });
38078
+ writeOwnerOnly(this.file, JSON.stringify(all, null, 2));
38079
+ }
38080
+ };
38081
+ var custodyChangedMessage = (vaultId) => `the server now says vault ${vaultId} is not Private, but this CLI has seen it Private before. The server's answer changed, so nothing was read or sent. Check the vault in the web app before writing to it.`;
38082
+ var PASSPHRASE_STDIN_NOT_PRIVATE = `--${PASSPHRASE_STDIN_FLAG} was given, but this vault is not Private. Nothing was read from stdin or sent: drop --${PASSPHRASE_STDIN_FLAG} to write to a Connected vault.`;
38083
+ async function resolveVaultCustody(client, vaultId, opts2) {
38084
+ const custody = await probeVaultCustody(client, vaultId);
38085
+ if (custody.private) {
38086
+ try {
38087
+ opts2.memory.remember(opts2.base, vaultId);
38088
+ } catch (e) {
38089
+ console.error(`warning: could not record vault ${vaultId} as Private: ${e.message}`);
38090
+ }
38091
+ return custody;
38092
+ }
38093
+ if (opts2.memory.seen(opts2.base, vaultId)) throw new PrivateVaultError(custodyChangedMessage(vaultId));
38094
+ if (opts2.passphraseStdin) throw new PrivateVaultError(PASSPHRASE_STDIN_NOT_PRIVATE);
38095
+ return custody;
38096
+ }
38097
+ var dataKeyChanged = (e) => e instanceof ApiError && e.status === 409 && e.slug === "data_key_changed";
38098
+ async function createSealedItem(client, vault, vaultId, current, meta, plain) {
38099
+ const id = crypto.randomUUID();
38100
+ const attempt = async (k) => {
38101
+ const { dataKey, key } = await privateItemDataKey(k, vault.key, vaultId, vault.enrolledAt);
38102
+ const fields = await sealPrivateItem({ id, vault_id: vaultId }, plain, dataKey);
38103
+ return client.createPrivateItem(vaultId, { ...meta, id, key, fields });
38104
+ };
38105
+ try {
38106
+ try {
38107
+ return await attempt(current);
38108
+ } catch (e) {
38109
+ if (!dataKeyChanged(e)) throw e;
38110
+ return await attempt(e.body?.key ?? null);
38111
+ }
38112
+ } catch (e) {
38113
+ if (e instanceof VaultKeyError) throw new PrivateVaultError(PRIVATE_VAULT_COPY.wrongPassphrase);
38114
+ throw e;
38115
+ }
38116
+ }
38117
+ async function updateSealedItem(client, vault, vaultId, credId, current, patch) {
38118
+ const plain = {};
38119
+ if (patch.value !== void 0) plain.value = patch.value;
38120
+ if (patch.notes !== void 0) plain.notes = patch.notes.trim() ? patch.notes : null;
38121
+ const meta = patch.name !== void 0 ? { name: patch.name } : {};
38122
+ if (!Object.keys(plain).length) return client.updatePrivateItem(vaultId, credId, meta);
38123
+ if (!vault) throw new PrivateVaultError("a Private item's value and notes can only be changed with the vault unlocked");
38124
+ const sealUnder = async (keyId, wrap) => {
38125
+ const dataKey = await unwrapVaultDataKey(wrap, vault.key, vaultId);
38126
+ const fields = await sealPrivateItem({ id: credId, vault_id: vaultId }, plain, dataKey);
38127
+ return client.updatePrivateItem(vaultId, credId, { ...meta, key_id: keyId, fields });
38128
+ };
38129
+ try {
38130
+ if (current) {
38131
+ try {
38132
+ return await sealUnder(current.key_id, current.client_wrapped_data_key);
38133
+ } catch (e) {
38134
+ if (!dataKeyChanged(e)) throw e;
38135
+ }
38136
+ }
38137
+ const { sealed } = await client.getPrivateItem(vaultId, credId);
38138
+ if (!sealed) throw new PrivateVaultError("this item is No-reveal, so its value cannot be changed from here.");
38139
+ return await sealUnder(sealed.key_id, sealed.client_wrapped_data_key);
38140
+ } catch (e) {
38141
+ if (e instanceof VaultKeyError) throw new PrivateVaultError("this item could not be sealed with your vault key.");
38142
+ throw e;
38143
+ }
38144
+ }
38145
+ function splitPassphraseAndValue(stdin) {
38146
+ const nl = stdin.search(/\r?\n/);
38147
+ if (nl < 0) throw new PrivateVaultError("stdin must carry the vault passphrase on its first line and the value after it");
38148
+ return { passphrase: stdin.slice(0, nl), value: stdin.slice(nl).replace(/^\r?\n/, "") };
38149
+ }
38150
+ var PRIVATE_IMPORT_REFUSED = "this vault is Private, and kt import cannot seal an import yet: the export file is parsed by the server, which would see every value in it. Nothing has been sent. Add each item with `kt cred add --value-stdin`.";
37526
38151
 
37527
38152
  // src/enroll.ts
37528
38153
  init_define_KT_HUB_FILES();
@@ -37732,10 +38357,47 @@ function parseHostsUnconfirmed(body) {
37732
38357
  return { count: typeof b.unconfirmedCount === "number" ? b.unconfirmedCount : hosts.length, hosts };
37733
38358
  }
37734
38359
 
38360
+ // src/exec-stdin.ts
38361
+ init_define_KT_HUB_FILES();
38362
+ var EXEC_STDIN_MAX_BYTES = 8 * 1024 * 1024;
38363
+ var StdinTooLargeError = class extends Error {
38364
+ constructor(maxBytes) {
38365
+ super(`stdin is larger than the ${maxBytes} byte limit for kt exec; nothing was sent. Copy larger input over SFTP instead.`);
38366
+ this.maxBytes = maxBytes;
38367
+ }
38368
+ };
38369
+ async function readExecStdin(input, maxBytes = EXEC_STDIN_MAX_BYTES) {
38370
+ const chunks = [];
38371
+ let total = 0;
38372
+ for await (const c of input) {
38373
+ const b = Buffer.isBuffer(c) ? c : Buffer.from(c);
38374
+ total += b.length;
38375
+ if (total > maxBytes) throw new StdinTooLargeError(maxBytes);
38376
+ chunks.push(b);
38377
+ }
38378
+ return total ? Buffer.concat(chunks) : void 0;
38379
+ }
38380
+ async function serverTakesExecStdin(base, fetchImpl = fetch) {
38381
+ try {
38382
+ const res = await fetchImpl(`${base.replace(/\/+$/, "")}/health`, { redirect: "error", signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });
38383
+ return (res.headers.get("kutrion-features") ?? "").split(",").some((v) => v.trim().toLowerCase() === "exec-stdin");
38384
+ } catch (e) {
38385
+ if (e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError")) throw new ExecStdinCheckTimedOut();
38386
+ return false;
38387
+ }
38388
+ }
38389
+ var HEALTH_TIMEOUT_MS = 1e4;
38390
+ var ExecStdinCheckTimedOut = class extends Error {
38391
+ constructor() {
38392
+ super("error: the check for kt exec --stdin support timed out; nothing was run");
38393
+ }
38394
+ };
38395
+ var STDIN_UNSUPPORTED = "error: this server does not support kt exec --stdin (it predates stdin support); nothing was run";
38396
+
37735
38397
  // src/transfer.ts
37736
38398
  init_define_KT_HUB_FILES();
37737
- var import_fs4 = require("fs");
37738
- var import_path5 = require("path");
38399
+ var import_fs6 = require("fs");
38400
+ var import_path6 = require("path");
37739
38401
  var import_promises = require("stream/promises");
37740
38402
  var EXIT_OK = 0;
37741
38403
  var EXIT_FAIL = 1;
@@ -37762,7 +38424,7 @@ function isSideError(s) {
37762
38424
  var listOn = (c, k, id, p) => k === "sftp" ? c.sftpList(id, p) : c.fileList(id, p);
37763
38425
  var mkdirOn = (c, k, id, p) => k === "sftp" ? c.sftpMkdir(id, p) : c.fileMkdir(id, p);
37764
38426
  var downloadOn = (c, k, id, p, o) => k === "sftp" ? c.sftpDownloadStream(id, p, o) : c.fileDownloadStream(id, p, o);
37765
- var uploadOn = (c, k, id, p, b) => k === "sftp" ? c.sftpUploadStream(id, p, b) : c.fileUploadStream(id, p, b);
38427
+ var uploadOn = (c, k, id, p, src) => k === "sftp" ? c.sftpUploadResumable(id, p, src) : c.fileUploadResumable(id, p, src);
37766
38428
  var abortOnHostKey = (e) => {
37767
38429
  if (hostKeyUnconfirmedFromError(e)) throw e;
37768
38430
  };
@@ -37808,6 +38470,8 @@ function exitFor(o) {
37808
38470
  return o.ok > 0 ? EXIT_PARTIAL : EXIT_FAIL;
37809
38471
  }
37810
38472
  var msg = (e) => e instanceof Error ? e.message : String(e);
38473
+ async function* emptyChunks() {
38474
+ }
37811
38475
  async function* counted(chunks, p, tally) {
37812
38476
  for await (const c of chunks) {
37813
38477
  tally.bytes += c.byteLength;
@@ -37821,15 +38485,15 @@ async function saveTo(s, local, append, p) {
37821
38485
  await (0, import_promises.pipeline)(counted(s.chunks, p, tally), process.stdout, { end: false });
37822
38486
  return tally.bytes;
37823
38487
  }
37824
- const dir = (0, import_path5.dirname)(local);
37825
- if (dir && dir !== "." && !(0, import_fs4.existsSync)(dir)) (0, import_fs4.mkdirSync)(dir, { recursive: true });
37826
- await (0, import_promises.pipeline)(counted(s.chunks, p, tally), (0, import_fs4.createWriteStream)(local, append ? { flags: "a" } : {}));
38488
+ const dir = (0, import_path6.dirname)(local);
38489
+ if (dir && dir !== "." && !(0, import_fs6.existsSync)(dir)) (0, import_fs6.mkdirSync)(dir, { recursive: true });
38490
+ await (0, import_promises.pipeline)(counted(s.chunks, p, tally), (0, import_fs6.createWriteStream)(local, append ? { flags: "a" } : {}));
37827
38491
  return tally.bytes;
37828
38492
  }
37829
38493
  async function getOne(c, kind, id, remote, local, opts2) {
37830
38494
  let start = 0;
37831
- if (opts2.resume && local !== "-" && (0, import_fs4.existsSync)(local)) {
37832
- const st = (0, import_fs4.statSync)(local);
38495
+ if (opts2.resume && local !== "-" && (0, import_fs6.existsSync)(local)) {
38496
+ const st = (0, import_fs6.statSync)(local);
37833
38497
  if (st.isFile()) start = st.size;
37834
38498
  }
37835
38499
  let s;
@@ -37852,7 +38516,7 @@ async function getOne(c, kind, id, remote, local, opts2) {
37852
38516
  opts2.note?.(`${remote}: server ignored the resume range, downloading from the start`);
37853
38517
  start = 0;
37854
38518
  }
37855
- const p = opts2.quiet ? NO_PROGRESS : progressBar((0, import_path5.basename)(remote) || remote, s.total);
38519
+ const p = opts2.quiet ? NO_PROGRESS : progressBar((0, import_path6.basename)(remote) || remote, s.total);
37856
38520
  const bytes = await saveTo(s, local, start > 0, p);
37857
38521
  p.finish();
37858
38522
  return { bytes };
@@ -37904,13 +38568,13 @@ async function getPath(c, kind, id, remote, local, opts2 = {}) {
37904
38568
  return out;
37905
38569
  }
37906
38570
  for await (const item of walkRemote(c, kind, id, remote)) {
37907
- const dest = (0, import_path5.join)(local, item.rel);
38571
+ const dest = (0, import_path6.join)(local, item.rel);
37908
38572
  if (item.link) {
37909
38573
  opts2.note?.(`skipped symlink ${item.path}`);
37910
38574
  continue;
37911
38575
  }
37912
38576
  if (item.dir) {
37913
- (0, import_fs4.mkdirSync)(dest, { recursive: true });
38577
+ (0, import_fs6.mkdirSync)(dest, { recursive: true });
37914
38578
  continue;
37915
38579
  }
37916
38580
  try {
@@ -37926,8 +38590,8 @@ async function getPath(c, kind, id, remote, local, opts2 = {}) {
37926
38590
  return out;
37927
38591
  }
37928
38592
  function* walkLocal(root, rel = "") {
37929
- for (const e of (0, import_fs4.readdirSync)(root, { withFileTypes: true })) {
37930
- const abs = (0, import_path5.join)(root, e.name);
38593
+ for (const e of (0, import_fs6.readdirSync)(root, { withFileTypes: true })) {
38594
+ const abs = (0, import_path6.join)(root, e.name);
37931
38595
  const r = rel ? `${rel}/${e.name}` : e.name;
37932
38596
  if (e.isSymbolicLink()) continue;
37933
38597
  if (e.isDirectory()) {
@@ -37939,18 +38603,21 @@ function* walkLocal(root, rel = "") {
37939
38603
  }
37940
38604
  }
37941
38605
  async function putOne(c, kind, id, local, remote, opts2) {
37942
- const total = (0, import_fs4.statSync)(local).size;
37943
- await c.preflightUpload?.(total);
37944
- const p = opts2.quiet ? NO_PROGRESS : progressBar((0, import_path5.basename)(local), total);
38606
+ const total = (0, import_fs6.statSync)(local).size;
38607
+ const p = opts2.quiet ? NO_PROGRESS : progressBar((0, import_path6.basename)(local), total);
37945
38608
  const tally = { bytes: 0 };
37946
- const r = await uploadOn(c, kind, id, remote, counted((0, import_fs4.createReadStream)(local), p, tally));
38609
+ const source = {
38610
+ size: total,
38611
+ slice: (start, end) => counted(end > start ? (0, import_fs6.createReadStream)(local, { start, end: end - 1 }) : emptyChunks(), p, tally)
38612
+ };
38613
+ const r = await uploadOn(c, kind, id, remote, source);
37947
38614
  p.finish();
37948
38615
  if (!r.streamed) opts2.note?.(`${local}: this runtime could not stream the request body, the file was buffered`);
37949
38616
  return r.size ?? tally.bytes;
37950
38617
  }
37951
38618
  async function putPath(c, kind, id, local, remote, opts2 = {}) {
37952
38619
  const out = empty();
37953
- const st = (0, import_fs4.existsSync)(local) ? (0, import_fs4.statSync)(local) : null;
38620
+ const st = (0, import_fs6.existsSync)(local) ? (0, import_fs6.statSync)(local) : null;
37954
38621
  if (!st) {
37955
38622
  out.failed++;
37956
38623
  out.errors.push(`${local}: no such file`);
@@ -37983,7 +38650,7 @@ async function putPath(c, kind, id, local, remote, opts2 = {}) {
37983
38650
  }
37984
38651
  continue;
37985
38652
  }
37986
- const parent = (0, import_path5.dirname)(dest);
38653
+ const parent = (0, import_path6.dirname)(dest);
37987
38654
  if (!made.has(parent)) {
37988
38655
  try {
37989
38656
  await mkdirOn(c, kind, id, parent);
@@ -38002,13 +38669,32 @@ async function putPath(c, kind, id, local, remote, opts2 = {}) {
38002
38669
  }
38003
38670
  return out;
38004
38671
  }
38672
+ var JOB_TERMINAL = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled"]);
38673
+ function transientPollError(e) {
38674
+ const status = e?.status;
38675
+ if (typeof status === "number") return status >= 500 || e.name === "ApiUnavailableError";
38676
+ return unreachedApi(e);
38677
+ }
38678
+ function unreachedApi(e) {
38679
+ if (e?.name === "ApiUnavailableError") return true;
38680
+ if (typeof e?.status === "number") return false;
38681
+ return e instanceof TypeError || e instanceof Error && /timed out|aborted|ECONN|ENOTFOUND|EAI_AGAIN|socket|fetch failed/i.test(e.message);
38682
+ }
38005
38683
  async function waitForJob(get, id, opts2) {
38006
38684
  const interval = opts2.intervalMs ?? 2e3;
38007
38685
  const deadline = Date.now() + opts2.timeoutMs;
38008
38686
  for (; ; ) {
38009
- const { job } = await get(id);
38010
- opts2.onTick?.(job);
38011
- if (job.status === "succeeded" || job.status === "failed") return job;
38687
+ let job;
38688
+ try {
38689
+ ({ job } = await get(id));
38690
+ } catch (e) {
38691
+ if (!transientPollError(e)) throw e;
38692
+ opts2.onRetry?.(e);
38693
+ }
38694
+ if (job) {
38695
+ opts2.onTick?.(job);
38696
+ if (JOB_TERMINAL.has(job.status)) return job;
38697
+ }
38012
38698
  if (Date.now() + interval > deadline) return null;
38013
38699
  await new Promise((r) => setTimeout(r, interval));
38014
38700
  }
@@ -38022,11 +38708,12 @@ function parseDailyAt(v) {
38022
38708
  }
38023
38709
 
38024
38710
  // src/index.ts
38025
- var CONFIG = (0, import_path6.join)((0, import_os5.homedir)(), ".config", "kutrion", "config.json");
38711
+ var CONFIG = (0, import_path7.join)((0, import_os5.homedir)(), ".config", "kutrion", "config.json");
38712
+ var PRIVATE_VAULTS = new PrivateVaultMemory((0, import_path7.join)((0, import_os5.homedir)(), ".config", "kutrion", "private-vaults.json"));
38026
38713
  function storedBase() {
38027
38714
  let file;
38028
38715
  try {
38029
- file = JSON.parse((0, import_fs5.readFileSync)(CONFIG, "utf8"));
38716
+ file = JSON.parse((0, import_fs7.readFileSync)(CONFIG, "utf8"));
38030
38717
  } catch {
38031
38718
  return "";
38032
38719
  }
@@ -38044,9 +38731,17 @@ function storedBase() {
38044
38731
  }
38045
38732
  return base;
38046
38733
  }
38047
- function saveCfg(base) {
38048
- (0, import_fs5.mkdirSync)((0, import_path6.dirname)(CONFIG), { recursive: true });
38049
- (0, import_fs5.writeFileSync)(CONFIG, JSON.stringify({ base }, null, 2));
38734
+ function saveCfg(base, fallbacks = readFallbacks(CONFIG, base)) {
38735
+ (0, import_fs7.mkdirSync)((0, import_path7.dirname)(CONFIG), { recursive: true });
38736
+ (0, import_fs7.writeFileSync)(CONFIG, JSON.stringify({ base, ...fallbacks.length ? { fallbacks } : {} }, null, 2));
38737
+ }
38738
+ var fallbackInstalled = false;
38739
+ function installBaseFallback(base) {
38740
+ if (fallbackInstalled) return;
38741
+ const list = readFallbacks(CONFIG, base);
38742
+ if (!list.length) return;
38743
+ fallbackInstalled = true;
38744
+ globalThis.fetch = fallbackFetch(globalThis.fetch, base, list);
38050
38745
  }
38051
38746
  function resolveBase(flag) {
38052
38747
  const explicit = typeof flag === "string" ? flag.trim() : "";
@@ -38193,16 +38888,38 @@ async function readStdin() {
38193
38888
  function readSecret(q) {
38194
38889
  return process.stdin.isTTY ? prompt(q, true) : readStdin();
38195
38890
  }
38891
+ var EMPTY_VALUE_MESSAGE = "The credential value is empty, so nothing was sent. Pipe one in with --value-stdin, or leave the flag off and type it at the prompt.";
38196
38892
  async function openPrivateItem2(client, fromStdin, vaultId, credId) {
38197
38893
  try {
38198
38894
  const key = await unlockForCommand(client, { tty: !!process.stdin.isTTY, fromStdin, ask: promptSecret, readStdin });
38199
- console.log((await openSealedItem(client, key, vaultId, credId)).value);
38895
+ const item = await openSealedItem(client, key, vaultId, credId);
38896
+ console.log(item.value);
38897
+ if (item.notes) console.error(`notes: ${item.notes}`);
38200
38898
  return null;
38201
38899
  } catch (e) {
38202
38900
  if (e instanceof PrivateVaultError) return e.message;
38203
38901
  throw e;
38204
38902
  }
38205
38903
  }
38904
+ async function unlockForSealedWrite(client, flags, needValue) {
38905
+ const passStdin = flags[PASSPHRASE_STDIN_FLAG] === true, valueStdin = flags["value-stdin"] === true;
38906
+ const io = { tty: !!process.stdin.isTTY, fromStdin: passStdin, ask: promptSecret, readStdin };
38907
+ if (passStdin && needValue) {
38908
+ if (!valueStdin) throw new PrivateVaultError(`with --${PASSPHRASE_STDIN_FLAG}, pipe the value too with --value-stdin: stdin carries the passphrase on its first line and the value after it`);
38909
+ const piped = splitPassphraseAndValue(await readStdin());
38910
+ if (piped.value === "") throw new PrivateVaultError(EMPTY_VALUE_MESSAGE);
38911
+ return { vault: await unlockVault(client, { ...io, readStdin: async () => piped.passphrase }), value: piped.value };
38912
+ }
38913
+ const vault = await unlockVault(client, io);
38914
+ if (!needValue) return { vault };
38915
+ const value = valueStdin ? await readSecret("value: ") : await prompt("value: ", true);
38916
+ if (value === "") throw new PrivateVaultError(EMPTY_VALUE_MESSAGE);
38917
+ return { vault, value };
38918
+ }
38919
+ var WATCHER_EVERY_MIN = 60;
38920
+ var WATCHER_EVERY_MAX = 86400;
38921
+ var WATCHER_PATTERN_MAX = 256;
38922
+ var withNotesMarker = (c, notes) => notes === void 0 ? c : { ...c, has_notes: notes.length > 0, notes_length: notes.length };
38206
38923
  function refuseSecretFlag(flag, instead) {
38207
38924
  console.error(`error: --${flag} is not accepted: it puts a credential on the command line, where shell history and the process list keep it.`);
38208
38925
  console.error(` ${instead}`);
@@ -38277,12 +38994,12 @@ async function credDest(client, args, flags, asJson, die) {
38277
38994
  const secret = await promptSecret(`current value for "${name}": `);
38278
38995
  let added;
38279
38996
  try {
38280
- added = await client.addCredentialDestination(vaultId, cid, {
38997
+ added = await withStepUp(client, () => client.addCredentialDestination(vaultId, cid, {
38281
38998
  protocol: group.key,
38282
38999
  host: host2,
38283
39000
  ...portGiven ? { port } : {},
38284
39001
  secret
38285
- });
39002
+ }));
38286
39003
  } catch (e) {
38287
39004
  const text2 = destRefusalText(e);
38288
39005
  if (text2) return die(text2);
@@ -38305,24 +39022,24 @@ async function credDest(client, args, flags, asJson, die) {
38305
39022
  const group = groupFor(row.protocol);
38306
39023
  if (!group) return die(`this destination is recorded on protocol ${row.protocol}, which this version of kt cannot withdraw`);
38307
39024
  const inGroup = entriesFor(group, before.destinations);
38308
- const entry = inGroup.find((x) => x.rows.some((r) => r.id === destId));
38309
- if (!entry) return die(`no destination ${destId} on "${name}"`);
39025
+ const entry2 = inGroup.find((x) => x.rows.some((r) => r.id === destId));
39026
+ if (!entry2) return die(`no destination ${destId} on "${name}"`);
38310
39027
  const sameGroupRows = inGroup.reduce((n, x) => n + x.rows.length, 0);
38311
- for (const line of removalWarning(name, group, entry, before.destinations.length - entry.rows.length, sameGroupRows - entry.rows.length)) {
39028
+ for (const line of removalWarning(name, group, entry2, before.destinations.length - entry2.rows.length, sameGroupRows - entry2.rows.length)) {
38312
39029
  console.error(line);
38313
39030
  }
38314
39031
  console.error("");
38315
39032
  if (!await confirmTyped("Type yes to withdraw it: ")) return die("nothing was changed");
38316
39033
  const secret = await promptSecret(`current value for "${name}": `);
38317
39034
  const gone = [];
38318
- for (const r of entry.rows) {
39035
+ for (const r of entry2.rows) {
38319
39036
  try {
38320
- await client.removeCredentialDestination(vaultId, cid, r.id, secret);
39037
+ await withStepUp(client, () => client.removeCredentialDestination(vaultId, cid, r.id, secret));
38321
39038
  gone.push(r.protocol);
38322
39039
  } catch (e) {
38323
39040
  if (gone.length) {
38324
- const left = entry.rows.filter((x) => !gone.includes(x.protocol)).map((x) => PROTOCOL_LABEL[x.protocol]).join(" and ");
38325
- console.error(`Only part of this destination was withdrawn. ${gone.map((pr) => PROTOCOL_LABEL[pr]).join(" and ")} at ${entry.host}:${entry.port} is no longer authorized; ${left} still is. Run the command again.`);
39041
+ const left = entry2.rows.filter((x) => !gone.includes(x.protocol)).map((x) => PROTOCOL_LABEL[x.protocol]).join(" and ");
39042
+ console.error(`Only part of this destination was withdrawn. ${gone.map((pr) => PROTOCOL_LABEL[pr]).join(" and ")} at ${entry2.host}:${entry2.port} is no longer authorized; ${left} still is. Run the command again.`);
38326
39043
  }
38327
39044
  const text2 = destRefusalText(e);
38328
39045
  if (text2) return die(text2);
@@ -38331,7 +39048,7 @@ async function credDest(client, args, flags, asJson, die) {
38331
39048
  }
38332
39049
  const after = await list();
38333
39050
  if (!asJson) {
38334
- console.log(after.destinations.length === 0 ? `"${name}" is no longer restricted. It is accepted at any host on any protocol again.` : `withdrew ${entry.host}:${entry.port}`);
39051
+ console.log(after.destinations.length === 0 ? `"${name}" is no longer restricted. It is accepted at any host on any protocol again.` : `withdrew ${entry2.host}:${entry2.port}`);
38335
39052
  console.log("");
38336
39053
  }
38337
39054
  return void await printList();
@@ -38343,7 +39060,33 @@ var ROLES = ["OWNER", "ADMIN", "EDITOR", "VIEWER"];
38343
39060
  var TOKEN_SCOPES = ["scim", "events"];
38344
39061
  var ORG_ROLES = ["owner", "admin", "member"];
38345
39062
  var ROTATION_SERVICES = ["linux_password", "postgres_password", "mysql_password"];
38346
- var VERSION = true ? "1.2.14" : "dev";
39063
+ var VERSION = true ? "1.2.16" : "dev";
39064
+ var CMD_HELP = {
39065
+ "ci-login": `kt ci-login --trust <id>
39066
+
39067
+ Exchange this CI job's own OIDC token for a short-lived agent and print its token on stdout. Nothing
39068
+ is stored: export it as KT_AGENT_TOKEN for \`kt run\`.
39069
+
39070
+ The token comes from KT_ID_TOKEN (GitLab id_tokens) or the GitHub Actions token service. The trust
39071
+ named by --trust (or KT_OIDC_TRUST_ID) accepts it only when all three claims match what was registered:
39072
+ iss the issuer: the CI instance URL, for example https://gitlab.example.com
39073
+ aud the audience: the aud your id_tokens block (or GitHub audience) requests
39074
+ sub the subject: matched against the trust's pattern, which names the project and optionally the ref
39075
+ (GitLab: project_path:group/repo:ref_type:branch:ref:main)
39076
+
39077
+ An org owner or admin registers a trust with \`kt trust add\`.
39078
+ Flags: --trust <id>, --json (print the whole exchange result instead of only the token).`,
39079
+ trust: `kt trust add --issuer <url> --audience <aud> --subject <pattern> --vault <id> [--vault <id> ..]
39080
+ [--name <n>] [--scopes vault.list,vault.use] [--egress lan|cloud] [--ttl <min>]
39081
+ kt trust ls
39082
+ kt trust rm <id>
39083
+
39084
+ A trust is the rule \`kt ci-login\` checks a CI job's OIDC token against: issuer, audience and subject
39085
+ pattern. --vault is required and repeatable: a trust reaches only the vaults named, never the whole
39086
+ organization. Org owners and admins only; granting vault.reveal or write scopes asks for step-up.
39087
+ --egress defaults to lan (output stays on the local network); pass --egress cloud to allow cloud AI.
39088
+ A subject pattern that matches every project is refused.`
39089
+ };
38347
39090
  var HELP = `kt \u2014 Kutrion CLI
38348
39091
  kt login [--email <e>] [--password-stdin] [--code <2fa>] prompts for the password, or reads it from stdin;
38349
39092
  with no --email or --password-stdin, first asks the Kutrion desktop app
@@ -38361,8 +39104,10 @@ var HELP = `kt \u2014 Kutrion CLI
38361
39104
 
38362
39105
  kt creds <vaultId> list credentials in a vault
38363
39106
  kt get <vaultId> <credId> [--reveal] [--vault-passphrase-stdin] show a credential (the value only with --reveal); a Private vault asks for its passphrase
38364
- kt cred add <vaultId> --name <n> [--value-stdin] [--type PASSWORD] [--notes ..] prompts for the value, or reads it from stdin
38365
- kt cred set <vaultId> <credId> [--name ..] [--value-stdin] [--notes ..]
39107
+ kt cred add <vaultId> --name <n> [--value-stdin] [--vault-passphrase-stdin] [--type PASSWORD] [--notes ..] prompts for the value, or reads it from stdin
39108
+ kt cred set <vaultId> <credId> [--name ..] [--value-stdin] [--vault-passphrase-stdin] [--notes ..]
39109
+ in a Private vault the value and notes are sealed here; with both
39110
+ stdin flags, stdin is the passphrase line, then the value
38366
39111
  kt cred rm <vaultId> <credId> delete a credential
38367
39112
  kt cred dest ls <vaultId> <credId> where this credential may be presented
38368
39113
  kt cred dest add <vaultId> <credId> --host <h> [--protocol SSH] [--port N]
@@ -38372,10 +39117,30 @@ var HELP = `kt \u2014 Kutrion CLI
38372
39117
  kt export <vaultId> [file] export a vault's credentials (plaintext, written mode 0600)
38373
39118
 
38374
39119
  kt mcp install --client <id> [--egress lan] connect an AI client (claude-code|claude-desktop|cursor|copilot)
39120
+ [--vault <id|name>]... [--connection <id>]... limit the agent to these vaults/connections (repeatable;
39121
+ none given = everything its owner can reach; unknown names refuse before minting)
39122
+ kt agent mint [--scope-vault <id|name>]... [--scope-conn <id>]... [--write-env <file>]
39123
+ mint an agent for a server install; --write-env writes
39124
+ KT_AGENT_TOKEN/KT_API_BASE to a 0600 file and never prints the token
38375
39125
  kt mcp status report each client's agent and which clients are wired
38376
39126
  kt mcp uninstall [--client <id>] revoke the agent and remove the client config entry
38377
39127
  kt mcp serve MCP over stdio (what the client config invokes)
39128
+ kt config set base-fallback <url>[,<url>...] [--allow-insecure-lan]
39129
+ other addresses for the same server, tried in order only when the
39130
+ primary cannot be reached (connection failure or a 52x from the edge,
39131
+ never an HTTP error from the API). Must be https unless the base is
39132
+ opted in with --allow-insecure-lan, which warns on stderr per request.
39133
+ Tokens carry no host or issuer claim (HS256 against the deployment's
39134
+ signing key), so any address reaching the SAME deployment accepts them and
39135
+ refresh follows whichever base answers; a different deployment refuses
39136
+ them. Websocket sessions (shell, forward) do not fall back.
39137
+ kt config get|unset base-fallback show or clear that list
38378
39138
  kt ci-login --trust <id> exchange this CI job's OIDC token for an agent (no stored credential)
39139
+ kt trust add --issuer <url> --audience <aud> --subject <pattern> --vault <id> [--vault <id> ..]
39140
+ register the CI identity ci-login accepts (org owner or admin)
39141
+ [--name <n>] [--scopes vault.list,vault.use] [--egress lan|cloud] [--ttl <min>]
39142
+ kt trust ls list the registered trusts
39143
+ kt trust rm <id> revoke a trust (no new exchange; issued agents run out their lifetime)
38379
39144
 
38380
39145
  kt conns list SSH connections
38381
39146
  kt ssh <connId> interactive SSH shell (Ctrl-] to detach)
@@ -38387,7 +39152,10 @@ var HELP = `kt \u2014 Kutrion CLI
38387
39152
  --insert places it at the prompt without pressing Enter
38388
39153
  kt demo [start] brokered sandbox session, then the audit trail + AI summary
38389
39154
  kt demo status | reset demo availability / remove everything it provisioned
38390
- kt exec <connId> <command...> run one command via the audited broker
39155
+ kt exec <connId> [--stdin] [--] <command...> run one command via the audited broker; --stdin sends
39156
+ this process's stdin (up to 8 MiB) to the command, never
39157
+ to its argv. Without it stdin is not read. kt's flags end at the
39158
+ connId (or the first --); the rest goes to the remote verbatim
38391
39159
  kt forward <connId> <localPort>:<dstHost>:<dstPort> (ssh -L style tunnel)
38392
39160
 
38393
39161
  kt sftp ls <connId> [path] list a remote directory
@@ -38414,7 +39182,8 @@ var HELP = `kt \u2014 Kutrion CLI
38414
39182
  A "side" below is written <sftp|file|cloud>:<connId>:<path> \u2014 one argument for one endpoint.
38415
39183
 
38416
39184
  kt transfer <from> <to> [--dir] remote-to-remote, brokered by the server
38417
- [--conflict overwrite|skip|rename] [--throttle <bytes/sec>]
39185
+ [--conflict overwrite|skip|rename] [--throttle <bytes/sec>] [--timeout <sec>]
39186
+ a <to> with no trailing slash whose parent exists names the file
38418
39187
  kt sync <from> <to> [--newer-only] directory sync
38419
39188
  [--mirror] [--yes] [--dry-run] --mirror DELETES extra files at the destination
38420
39189
 
@@ -38423,6 +39192,7 @@ var HELP = `kt \u2014 Kutrion CLI
38423
39192
  kt job new <from> <to> [--wait] queue a job (--wait polls to completion)
38424
39193
  [--dir] [--verify] [--throttle N] [--newer-only] [--mirror --yes] [--timeout <sec>]
38425
39194
  kt job wait <id> [--timeout <sec>] poll an existing job
39195
+ kt job cancel <id> cancel a queued or running job
38426
39196
 
38427
39197
  kt schedule [ls] recurring transfers
38428
39198
  kt schedule new <name> <from> <to> --every <sec> or --daily-at HH:MM (UTC)
@@ -38431,6 +39201,7 @@ var HELP = `kt \u2014 Kutrion CLI
38431
39201
 
38432
39202
  kt watcher [ls] folder watchers (fire on new/changed files)
38433
39203
  kt watcher new <name> <source> <dest> [--pattern <glob>] [--every <sec>]
39204
+ --every is 60..86400 seconds (default 300); --pattern is a filename glob (* and ?), up to 256 characters, matched on the name only
38434
39205
  [--min-size N] [--max-size N] [--min-age <sec>] [--no-stability]
38435
39206
  kt watcher enable|disable|rm <id> pause / resume / delete a watcher
38436
39207
 
@@ -38545,7 +39316,7 @@ var HELP = `kt \u2014 Kutrion CLI
38545
39316
  [--reporter console|junit|json] [--out <file>] [--dry-run]
38546
39317
  with KT_AGENT_TOKEN set, runs as that pipeline principal over /mcp
38547
39318
 
38548
- kt history [query] [--limit N] searchable command history
39319
+ kt history [query] [--limit N] searchable history of brokered exec commands, such as kt exec; not shell input
38549
39320
  [--session <id>] [--host <connection id>] [--since <iso>] [--until <iso>]
38550
39321
  kt news security news feed
38551
39322
 
@@ -38565,6 +39336,7 @@ var HELP = `kt \u2014 Kutrion CLI
38565
39336
  kt mfa verify --code <code> finish TOTP enrollment
38566
39337
  kt mfa disable turn off TOTP
38567
39338
  kt mfa backup-codes regenerate backup codes
39339
+ kt step-up prove app-MFA now (code is prompted), then re-run the refused command
38568
39340
  kt passkeys registered passkeys / security keys
38569
39341
  kt passkey rm <id> remove a passkey
38570
39342
 
@@ -38690,13 +39462,17 @@ function refusedSavedSession(e, argv2) {
38690
39462
  }
38691
39463
  }
38692
39464
  async function main() {
38693
- const { cmd, positionals, flags } = parseArgs(process.argv.slice(2));
39465
+ const { cmd, positionals, flags, multi } = parseArgs(process.argv.slice(2));
38694
39466
  const asJson = flags.json === true;
38695
39467
  const die = (msg2) => {
38696
39468
  console.error(msg2);
38697
39469
  process.exit(1);
38698
39470
  };
38699
39471
  const unknown = unknownFlags(flags);
39472
+ if (flags.help === true && cmd !== "help") {
39473
+ console.log(CMD_HELP[cmd] ?? HELP);
39474
+ return;
39475
+ }
38700
39476
  if (unknown.length) die(`unknown flag: ${unknown.map((f) => `--${f}`).join(", ")}
38701
39477
  run \`kt --help\` for the flags each command takes`);
38702
39478
  if (cmd === "--version" || cmd === "-v" || cmd === "version") {
@@ -38707,6 +39483,7 @@ run \`kt --help\` for the flags each command takes`);
38707
39483
  const configured = resolveBase(flags.base);
38708
39484
  if (!configured && !offlineLint && cmd !== "help" && cmd !== "--help" && cmd !== "-h") die(NO_BASE);
38709
39485
  const cfg = { base: configured };
39486
+ if (!offlineLint && configured) installBaseFallback(configured);
38710
39487
  let issued;
38711
39488
  let signedThisRun = false;
38712
39489
  const ensureRefreshKeyBound = async () => {
@@ -38732,8 +39509,11 @@ run \`kt --help\` for the flags each command takes`);
38732
39509
  });
38733
39510
  if (outcome !== "failed" && borrowedDevice(cfg.base, kcSlots)?.id === dev.id) settleRefreshBind(cfg.base, kcSlots, dev.id);
38734
39511
  };
39512
+ const longCall = cmd === "transfer" || cmd === "sync";
39513
+ const longTimeoutSec = Number(flags.timeout);
38735
39514
  const client = new KutrionClient({
38736
39515
  baseUrl: cfg.base,
39516
+ ...longCall ? { timeoutMs: (Number.isFinite(longTimeoutSec) && longTimeoutSec > 0 ? longTimeoutSec : 6 * 3600) * 1e3 } : {},
38737
39517
  getToken: () => getToken(cfg.base),
38738
39518
  onToken: (t) => {
38739
39519
  issued = t || void 0;
@@ -38780,6 +39560,14 @@ run \`kt --help\` for the flags each command takes`);
38780
39560
  });
38781
39561
  const kc = { get: getToken, set: setToken, clear: clearToken };
38782
39562
  const hubCall = hubCallFor(cfg.base);
39563
+ const vaultCustody = async (vaultId) => {
39564
+ try {
39565
+ return await resolveVaultCustody(client, vaultId, { base: cfg.base, memory: PRIVATE_VAULTS, passphraseStdin: flags[PASSPHRASE_STDIN_FLAG] === true });
39566
+ } catch (e) {
39567
+ if (e instanceof PrivateVaultError) return die(e.message);
39568
+ throw e;
39569
+ }
39570
+ };
38783
39571
  const connectionContext = async (connId) => {
38784
39572
  try {
38785
39573
  const rows = (await client.listConnections()).connections;
@@ -38855,6 +39643,10 @@ run \`kt --help\` for the flags each command takes`);
38855
39643
  timeoutMs: (intFlag("timeout") ?? 3600) * 1e3,
38856
39644
  onTick: (j) => {
38857
39645
  if (ticker) process.stderr.write(`\r${j.status} \u2014 ${j.files} file(s), ${fmtBytes(j.bytes)} `);
39646
+ },
39647
+ onRetry: (e) => {
39648
+ if (!asJson) process.stderr.write(`${ticker ? "\n" : ""}could not reach the server (${e instanceof Error ? e.message : String(e)}); still waiting
39649
+ `);
38858
39650
  }
38859
39651
  });
38860
39652
  if (ticker) process.stderr.write("\n");
@@ -38864,8 +39656,8 @@ run \`kt --help\` for the flags each command takes`);
38864
39656
  process.exit(EXIT_UNKNOWN);
38865
39657
  }
38866
39658
  show(job);
38867
- if (job.status === "failed") {
38868
- if (!asJson) console.error(`job failed: ${job.error ?? "no reason given"}`);
39659
+ if (job.status === "failed" || job.status === "canceled") {
39660
+ if (!asJson) console.error(`job ${job.status}: ${job.error ?? "no reason given"}`);
38869
39661
  process.exit(EXIT_FAIL);
38870
39662
  }
38871
39663
  process.exit(EXIT_OK);
@@ -38951,7 +39743,6 @@ run \`kt --help\` for the flags each command takes`);
38951
39743
  process.exit(1);
38952
39744
  }
38953
39745
  const staleDevice = dropBorrowed(cfg.base, kc);
38954
- if (staleDevice) await client.revokeDevice(staleDevice).catch(() => void 0);
38955
39746
  const post = fetchPost(cfg.base, refuseBodyRedirects());
38956
39747
  const registered = await registerOwnDevice({
38957
39748
  base: cfg.base,
@@ -38966,8 +39757,10 @@ run \`kt --help\` for the flags each command takes`);
38966
39757
  } catch {
38967
39758
  return null;
38968
39759
  }
38969
- })()
39760
+ })(),
39761
+ previousDeviceId: staleDevice
38970
39762
  }).catch(() => ({ kind: "skipped", note: null }));
39763
+ if (staleDevice) await client.revokeDevice(staleDevice).catch(() => void 0);
38971
39764
  recordSignIn(accountRoot, signedIn);
38972
39765
  recordLastAccountId(cfg.base, kc, signedIn);
38973
39766
  console.log(`logged in (token stored in ${backendName()})`);
@@ -38998,7 +39791,10 @@ run \`kt --help\` for the flags each command takes`);
38998
39791
  break;
38999
39792
  case "whoami":
39000
39793
  requireAuth();
39001
- show((await client.me()).user);
39794
+ {
39795
+ const { user } = await client.me();
39796
+ show(asJson ? user : whoamiLines(user));
39797
+ }
39002
39798
  break;
39003
39799
  case "vaults":
39004
39800
  requireAuth();
@@ -39007,8 +39803,13 @@ run \`kt --help\` for the flags each command takes`);
39007
39803
  case "vault": {
39008
39804
  requireAuth();
39009
39805
  const sub = positionals[0];
39010
- if (sub === "create" && positionals[1]) show((await client.createVault(positionals.slice(1).join(" "))).vault);
39011
- else if (sub === "rename" && positionals[1] && positionals[2]) show((await client.renameVault(positionals[1], positionals.slice(2).join(" "))).vault);
39806
+ if (sub === "create" && positionals[1]) {
39807
+ const name = positionals.slice(1).join(" ");
39808
+ const twin = await client.listVaults().then((r) => r.vaults.find((v) => !v.team_id && v.name === name), () => void 0);
39809
+ if (twin) console.error(`warning: a vault named "${name}" already exists (${twin.id}); creating another`);
39810
+ const { vault } = await client.createVault(name);
39811
+ show(asJson ? vault : `created vault "${vault.name}" (${vault.id})`);
39812
+ } else if (sub === "rename" && positionals[1] && positionals[2]) show((await client.renameVault(positionals[1], positionals.slice(2).join(" "))).vault);
39012
39813
  else if (sub === "rm" && positionals[1]) {
39013
39814
  await client.deleteVault(positionals[1]);
39014
39815
  console.log("deleted");
@@ -39037,12 +39838,15 @@ run \`kt --help\` for the flags each command takes`);
39037
39838
  noReveal = credential.value === null;
39038
39839
  } else {
39039
39840
  const summary = (await client.listCredentials(positionals[0])).credentials.find((c) => c.id === positionals[1]);
39040
- if (!summary) return die(`no credential ${positionals[1]} in vault ${positionals[0]}`);
39841
+ if (!summary) {
39842
+ console.error(`no credential ${positionals[1]} in vault ${positionals[0]}`);
39843
+ process.exit(EXIT_NOT_FOUND);
39844
+ }
39041
39845
  noReveal = summary.inject_only === true;
39042
39846
  if (noReveal) show({ ...summary, value: null });
39043
39847
  else {
39044
39848
  show({ ...summary, value_hidden: true });
39045
- console.error("value hidden; pass --reveal to print it");
39849
+ if (!asJson) console.error("value hidden; pass --reveal to print it");
39046
39850
  }
39047
39851
  }
39048
39852
  if (noReveal) {
@@ -39064,17 +39868,45 @@ run \`kt --help\` for the flags each command takes`);
39064
39868
  if (flags.value !== void 0) refuseSecretFlag("value", "pipe it to --value-stdin, or leave it off to be prompted");
39065
39869
  if (flags["value-stdin"] === true && typeof flags.name !== "string") die("--value-stdin needs --name, because stdin carries the value");
39066
39870
  const name = String(flags.name ?? await prompt("name: "));
39067
- const value = flags["value-stdin"] === true ? await readSecret("value: ") : await prompt("value: ", true);
39068
39871
  const notes = flags.notes !== void 0 ? String(flags.notes) : void 0;
39069
- show((await client.storeCredential(vaultId, { credential_type: type, name, value, notes })).credential);
39872
+ const custody = await vaultCustody(vaultId);
39873
+ if (custody.private) {
39874
+ try {
39875
+ const { vault, value: value2 } = await unlockForSealedWrite(client, flags, true);
39876
+ show(withNotesMarker((await createSealedItem(client, vault, vaultId, custody.key, { credential_type: type, name }, { value: value2, notes })).credential, notes));
39877
+ } catch (e) {
39878
+ if (e instanceof PrivateVaultError) return die(e.message);
39879
+ throw e;
39880
+ }
39881
+ break;
39882
+ }
39883
+ const value = flags["value-stdin"] === true ? await readSecret("value: ") : await prompt("value: ", true);
39884
+ if (value === "") die(EMPTY_VALUE_MESSAGE);
39885
+ show(withNotesMarker((await client.storeCredential(vaultId, { credential_type: type, name, value, notes })).credential, notes));
39070
39886
  } else if (sub === "set" && vaultId && positionals[2]) {
39071
39887
  if (flags.value !== void 0) refuseSecretFlag("value", "pipe it to --value-stdin");
39072
39888
  const patch = {};
39073
39889
  if (flags.name !== void 0) patch.name = String(flags.name);
39074
- if (flags["value-stdin"] === true) patch.value = await readSecret("value: ");
39075
39890
  if (flags.notes !== void 0) patch.notes = String(flags.notes);
39076
- if (!Object.keys(patch).length) return die("nothing to update \u2014 pass --name/--value-stdin/--notes");
39077
- show((await client.updateCredential(vaultId, positionals[2], patch)).credential);
39891
+ if (!Object.keys(patch).length && flags["value-stdin"] !== true) return die("nothing to update \u2014 pass --name/--value-stdin/--notes");
39892
+ const custody = await vaultCustody(vaultId);
39893
+ if (custody.private) {
39894
+ try {
39895
+ const sealsFields = flags["value-stdin"] === true || patch.notes !== void 0;
39896
+ const unlocked = sealsFields ? await unlockForSealedWrite(client, flags, flags["value-stdin"] === true) : null;
39897
+ if (unlocked?.value !== void 0) patch.value = unlocked.value;
39898
+ show(withNotesMarker((await updateSealedItem(client, unlocked?.vault ?? null, vaultId, positionals[2], custody.key, patch)).credential, patch.notes));
39899
+ } catch (e) {
39900
+ if (e instanceof PrivateVaultError) return die(e.message);
39901
+ throw e;
39902
+ }
39903
+ break;
39904
+ }
39905
+ if (flags["value-stdin"] === true) {
39906
+ patch.value = await readSecret("value: ");
39907
+ if (patch.value === "") die(EMPTY_VALUE_MESSAGE);
39908
+ }
39909
+ show(withNotesMarker((await client.updateCredential(vaultId, positionals[2], patch)).credential, patch.notes));
39078
39910
  } else if (sub === "rm" && vaultId && positionals[2]) {
39079
39911
  await client.deleteCredential(vaultId, positionals[2]);
39080
39912
  console.log("deleted");
@@ -39155,8 +39987,20 @@ run \`kt --help\` for the flags each command takes`);
39155
39987
  }
39156
39988
  case "exec": {
39157
39989
  requireAuth();
39158
- if (!positionals[0] || !positionals[1]) return die("usage: kt exec <connId> <command...>");
39159
- const r = await client.exec(positionals[0], positionals.slice(1).join(" ")).catch(async (e) => {
39990
+ if (!positionals[0] || !positionals[1]) return die("usage: kt exec <connId> [--stdin] [--] <command...>");
39991
+ let stdin;
39992
+ if (flags.stdin === true) {
39993
+ const takes = await serverTakesExecStdin(cfg.base).catch((e) => {
39994
+ if (e instanceof ExecStdinCheckTimedOut) return die(e.message);
39995
+ throw e;
39996
+ });
39997
+ if (!takes) return die(STDIN_UNSUPPORTED);
39998
+ stdin = await readExecStdin(process.stdin).catch((e) => {
39999
+ if (e instanceof StdinTooLargeError) return die(`error: ${e.message}`);
40000
+ throw e;
40001
+ });
40002
+ }
40003
+ const r = await client.exec(positionals[0], positionals.slice(1).join(" "), stdin?.toString("base64")).catch(async (e) => {
39160
40004
  const k = hostKeyUnconfirmedFromError(e);
39161
40005
  const chained = k ? null : hostKeyChainedFromError(e);
39162
40006
  if (!k && !chained) throw e;
@@ -39195,7 +40039,7 @@ run \`kt --help\` for the flags each command takes`);
39195
40039
  }
39196
40040
  let job;
39197
40041
  try {
39198
- job = parseJobFile((0, import_fs5.readFileSync)(path, "utf8"));
40042
+ job = parseJobFile((0, import_fs7.readFileSync)(path, "utf8"));
39199
40043
  } catch (e) {
39200
40044
  return die(e instanceof JobFileError ? `${path}: ${e.message}` : `${path}: ${e.message}`);
39201
40045
  }
@@ -39204,7 +40048,7 @@ run \`kt --help\` for the flags each command takes`);
39204
40048
  if (agentToken && !asJson && reporter === "console") {
39205
40049
  console.error(`running as a pipeline principal through ${cfg.base.replace(/\/+$/, "")}/mcp`);
39206
40050
  }
39207
- const result = await runJob(job, runnerClient, { readLocal: (p) => (0, import_fs5.readFileSync)(p), writeLocal: (p, d) => (0, import_fs5.writeFileSync)(p, d) }, {
40051
+ const result = await runJob(job, runnerClient, { readLocal: (p) => (0, import_fs7.readFileSync)(p), writeLocal: (p, d) => (0, import_fs7.writeFileSync)(p, d) }, {
39208
40052
  dryRun,
39209
40053
  onStep: (s) => {
39210
40054
  if (asJson || reporter !== "console") return;
@@ -39230,7 +40074,7 @@ run \`kt --help\` for the flags each command takes`);
39230
40074
  const path = positionals[2] ?? ".";
39231
40075
  const r = await client.sftpList(connId, path);
39232
40076
  if (asJson) return console.log(JSON.stringify(r, null, 2));
39233
- show(r.entries, ["name", "type", "size", "mode"]);
40077
+ show(r.entries.map((e) => ({ ...e, mode: modeString(e.mode, e.type) })), ["name", "type", "size", "mode"]);
39234
40078
  } else if (sub === "mkdir" && connId && positionals[2]) {
39235
40079
  await client.sftpMkdir(connId, positionals[2]);
39236
40080
  console.log("created");
@@ -39302,6 +40146,11 @@ run \`kt --help\` for the flags each command takes`);
39302
40146
  to,
39303
40147
  throttle_bps: intFlag("throttle"),
39304
40148
  ...conflict ? { conflict } : {}
40149
+ }).catch((e) => {
40150
+ if (!unreachedApi(e)) throw e;
40151
+ if (asJson) console.log(JSON.stringify({ status: "unknown", error: e instanceof Error ? e.message : String(e) }, null, 2));
40152
+ else console.error(`no answer from the server (${e instanceof Error ? e.message : String(e)}); the transfer may still be running or may have completed. Check the destination before running it again, or use \`kt job new\` for a transfer you can poll.`);
40153
+ return process.exit(EXIT_UNKNOWN);
39305
40154
  });
39306
40155
  if (asJson) return console.log(JSON.stringify(r, null, 2));
39307
40156
  console.log(`transferred ${r.files} file(s), ${fmtBytes(r.bytes)}${r.skipped ? `, ${r.skipped} skipped` : ""}${r.renamed ? `, ${r.renamed} renamed` : ""}`);
@@ -39350,8 +40199,10 @@ run \`kt --help\` for the flags each command takes`);
39350
40199
  return await watchJob(job.id);
39351
40200
  } else if (sub === "wait" && positionals[1]) {
39352
40201
  return await watchJob(positionals[1]);
40202
+ } else if (sub === "cancel" && positionals[1]) {
40203
+ show((await client.cancelTransferJob(positionals[1])).job);
39353
40204
  } else {
39354
- die("usage: kt job [ls] | kt job get <id> | kt job new <from> <to> [--wait] | kt job wait <id>");
40205
+ die("usage: kt job [ls] | kt job get <id> | kt job new <from> <to> [--wait] | kt job wait <id> | kt job cancel <id>");
39355
40206
  }
39356
40207
  break;
39357
40208
  }
@@ -39401,6 +40252,13 @@ run \`kt --help\` for the flags each command takes`);
39401
40252
  } else if (sub === "new" && positionals[1] && positionals[2] && positionals[3]) {
39402
40253
  const source = sideArg(positionals[2], "source");
39403
40254
  const dest = sideArg(positionals[3], "destination");
40255
+ const every = intFlag("every");
40256
+ if (flags.every === true || every !== void 0 && (!Number.isInteger(every) || every < WATCHER_EVERY_MIN || every > WATCHER_EVERY_MAX)) {
40257
+ return die(`--every must be a whole number of seconds from ${WATCHER_EVERY_MIN} to ${WATCHER_EVERY_MAX} (got ${flags.every === true ? "nothing" : flags.every})`);
40258
+ }
40259
+ if (flags.pattern !== void 0 && (flags.pattern === true || !String(flags.pattern).trim() || String(flags.pattern).length > WATCHER_PATTERN_MAX)) {
40260
+ return die(`--pattern needs a filename glob such as '*.csv', at most ${WATCHER_PATTERN_MAX} characters`);
40261
+ }
39404
40262
  show((await client.createFolderWatcher({
39405
40263
  name: positionals[1],
39406
40264
  source,
@@ -39414,7 +40272,7 @@ run \`kt --help\` for the flags each command takes`);
39414
40272
  // Stability checks are what stop a watcher picking up a file that is still being written.
39415
40273
  // Off is opt-in, never a default.
39416
40274
  stability_checks: flags["no-stability"] !== true,
39417
- interval_seconds: intFlag("every")
40275
+ interval_seconds: every
39418
40276
  })).watcher);
39419
40277
  } else if ((sub === "enable" || sub === "disable") && positionals[1]) {
39420
40278
  await client.setFolderWatcherEnabled(positionals[1], sub === "enable");
@@ -39486,11 +40344,11 @@ ${r.authorize_url}`);
39486
40344
  const buf = Buffer.from(await blob.arrayBuffer());
39487
40345
  if (local === "-") process.stdout.write(buf);
39488
40346
  else {
39489
- (0, import_fs5.writeFileSync)(local, buf);
40347
+ (0, import_fs7.writeFileSync)(local, buf);
39490
40348
  console.error(`saved ${local} (${fmtBytes(buf.length)})`);
39491
40349
  }
39492
40350
  } else if (sub === "put" && id && positionals[2] && positionals[3]) {
39493
- const buf = (0, import_fs5.readFileSync)(positionals[2]);
40351
+ const buf = (0, import_fs7.readFileSync)(positionals[2]);
39494
40352
  await client.cloudUpload(id, positionals[3], new Blob([buf]));
39495
40353
  console.error(`uploaded ${fmtBytes(buf.length)}`);
39496
40354
  } else if (sub === "mkdir" && id && positionals[2]) {
@@ -39596,17 +40454,20 @@ ${r.token}`);
39596
40454
  }
39597
40455
  const scopes = flags.scopes ? String(flags.scopes).split(",").map((s) => s.trim()).filter(Boolean) : DEFAULT_SCOPES;
39598
40456
  const ttlDays = flags.days !== void 0 ? Number(flags.days) : DEFAULT_TTL_DAYS;
39599
- const dir = (0, import_path6.resolve)(String(flags.dir ?? process.cwd()));
40457
+ const dir = (0, import_path7.resolve)(String(flags.dir ?? process.cwd()));
39600
40458
  const egress = String(flags.egress ?? "cloud");
39601
40459
  if (egress !== "lan" && egress !== "cloud") return die("--egress must be lan or cloud");
40460
+ if (flags.vault === true || flags.connection === true) return die("--vault and --connection each take a value (an id or a name)");
39602
40461
  try {
39603
- const r = await installClient({ base: cfg.base, userToken, client: client2, dir, scopes, ttlDays, egressClass: egress });
40462
+ const resourceScope = await resolveResourceScope(cfg.base, userToken, multi.vault ?? [], multi.connection ?? []);
40463
+ const r = await installClient({ base: cfg.base, userToken, client: client2, dir, scopes, ttlDays, egressClass: egress, resourceScope });
39604
40464
  warnLeftLive(r, client2);
39605
40465
  if (asJson) return console.log(JSON.stringify(r, null, 2));
39606
40466
  console.log(`installed ${CLIENTS[client2].label}`);
39607
40467
  console.log(` config ${r.configPath}`);
39608
40468
  console.log(` agent ${r.principalId} (expires ${r.expiresAt})`);
39609
40469
  console.log(` scopes ${r.scopes.join(", ")}`);
40470
+ console.log(` reach ${describeScope(r.resourceScope)}`);
39610
40471
  if (r.superseded) console.log(` revoked ${r.superseded} earlier agent(s) for this client`);
39611
40472
  console.log(` credential stored in the ${backendName()} \u2014 the config file does not contain it`);
39612
40473
  console.log(r.verified ? " verified connection to the broker works" : " warning: could not verify the connection");
@@ -39625,7 +40486,7 @@ next: ${r.next}`);
39625
40486
  break;
39626
40487
  }
39627
40488
  if (sub === "status") {
39628
- const dir = (0, import_path6.resolve)(String(flags.dir ?? process.cwd()));
40489
+ const dir = (0, import_path7.resolve)(String(flags.dir ?? process.cwd()));
39629
40490
  const s = await mcpStatus(cfg.base, dir);
39630
40491
  if (asJson) return console.log(JSON.stringify(s, null, 2));
39631
40492
  console.log(`credential store: ${s.keychain}`);
@@ -39645,6 +40506,7 @@ next: ${r.next}`);
39645
40506
  if (c.agent?.state !== "live") continue;
39646
40507
  const rl = c.agent.rate_limit;
39647
40508
  console.log(`${c.client}: scopes ${(c.agent.scopes ?? []).join(", ")}` + (rl?.tool_calls ? `; limit ${rl.tool_calls} tool calls / ${Math.round((rl.window_ms ?? 0) / 1e3)}s` : ""));
40509
+ console.log(`${c.client}: reach ${describeScope(c.agent.resource_scope)}`);
39648
40510
  }
39649
40511
  if (s.legacy) console.log(`
39650
40512
  shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.trimEnd());
@@ -39656,7 +40518,7 @@ shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.
39656
40518
  if (client2 && !CLIENT_IDS.includes(client2)) {
39657
40519
  return die(`unknown --client; one of: ${CLIENT_IDS.join(", ")}`);
39658
40520
  }
39659
- const dir = (0, import_path6.resolve)(String(flags.dir ?? process.cwd()));
40521
+ const dir = (0, import_path7.resolve)(String(flags.dir ?? process.cwd()));
39660
40522
  const r = await uninstallClient({ base: cfg.base, userToken, client: client2, dir });
39661
40523
  warnLeftLive(r, client2);
39662
40524
  if (asJson) return console.log(JSON.stringify(r, null, 2));
@@ -39665,7 +40527,7 @@ shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.
39665
40527
  break;
39666
40528
  }
39667
40529
  if (sub && sub !== "serve") {
39668
- return die("usage: kt mcp [serve] | kt mcp install --client <id> | kt mcp status | kt mcp uninstall [--client <id>]");
40530
+ return die("usage: kt mcp [serve] | kt mcp install --client <id> [--vault <id|name>]... [--connection <id>]... | kt mcp status | kt mcp uninstall [--client <id>]");
39669
40531
  }
39670
40532
  if (flags.token !== void 0) refuseSecretFlag("token", "export KT_AGENT_TOKEN instead, or run kt mcp install");
39671
40533
  let agentToken;
@@ -39686,6 +40548,136 @@ shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.
39686
40548
  await runMcpBridge({ base: cfg.base, token: agentToken });
39687
40549
  break;
39688
40550
  }
40551
+ // #3545: a principal for a server install, which has no keychain and no AI client config to write.
40552
+ case "agent": {
40553
+ if (positionals[0] !== "mint") {
40554
+ return die("usage: kt agent mint [--name <n>] [--scopes a,b] [--days <n>] [--egress lan|cloud] [--scope-vault <id|name>]... [--scope-conn <id|name>]... [--write-env <file>]");
40555
+ }
40556
+ const userToken = requireAuth();
40557
+ if (flags["scope-vault"] === true || flags["scope-conn"] === true) return die("--scope-vault and --scope-conn each take a value (an id or a name)");
40558
+ if (flags["write-env"] === true) return die("--write-env takes a file path");
40559
+ const scopes = flags.scopes ? String(flags.scopes).split(",").map((x) => x.trim()).filter(Boolean) : DEFAULT_SCOPES;
40560
+ const ttlDays = flags.days !== void 0 ? Number(flags.days) : DEFAULT_TTL_DAYS;
40561
+ const egress = String(flags.egress ?? "cloud");
40562
+ if (egress !== "lan" && egress !== "cloud") return die("--egress must be lan or cloud");
40563
+ const envFile = typeof flags["write-env"] === "string" ? (0, import_path7.resolve)(String(flags["write-env"])) : void 0;
40564
+ let created = false;
40565
+ if (envFile) {
40566
+ if ((0, import_fs7.existsSync)(envFile) && flags.yes !== true) return die(`${envFile} already exists; pass --yes to replace it (the principal it names stays live until revoked)`);
40567
+ created = !(0, import_fs7.existsSync)(envFile);
40568
+ try {
40569
+ writeOwnerOnly(envFile, "");
40570
+ } catch (e) {
40571
+ return die(`cannot write ${envFile}: ${e.message}`);
40572
+ }
40573
+ }
40574
+ try {
40575
+ const resourceScope = await resolveResourceScope(cfg.base, userToken, multi["scope-vault"] ?? [], multi["scope-conn"] ?? []);
40576
+ const m = await mintAgent({
40577
+ base: cfg.base,
40578
+ userToken,
40579
+ name: String(flags.name ?? `kt agent on ${(0, import_os5.hostname)()}`).slice(0, 120),
40580
+ purpose: "Server install minted with 'kt agent mint'",
40581
+ scopes,
40582
+ ttlDays,
40583
+ egressClass: egress,
40584
+ resourceScope
40585
+ });
40586
+ if (envFile) writeOwnerOnly(envFile, `KT_AGENT_TOKEN=${m.token}
40587
+ KT_API_BASE=${cfg.base}
40588
+ `);
40589
+ const view = { principal_id: m.id, expires_at: m.expires_at, scopes: m.scopes, resource_scope: resourceScope ?? null, ...envFile ? { env_file: envFile } : { token: m.token } };
40590
+ if (asJson) return console.log(JSON.stringify(view, null, 2));
40591
+ console.log(`minted agent ${m.id} (expires ${m.expires_at})`);
40592
+ console.log(` scopes ${m.scopes.join(", ")}`);
40593
+ console.log(` reach ${describeScope(resourceScope)}`);
40594
+ if (envFile) console.log(` wrote KT_AGENT_TOKEN and KT_API_BASE to ${envFile} (mode 0600); the token is not shown`);
40595
+ else console.log(` token ${m.token}
40596
+ shown once; export it as KT_AGENT_TOKEN`);
40597
+ } catch (e) {
40598
+ if (envFile && created) {
40599
+ try {
40600
+ (0, import_fs7.unlinkSync)(envFile);
40601
+ } catch {
40602
+ }
40603
+ }
40604
+ return die(`mint failed: ${e.message}`);
40605
+ }
40606
+ break;
40607
+ }
40608
+ // #3541: the OIDC trusts `kt ci-login` exchanges against. The routes are owner/admin only and the
40609
+ // server refuses a pattern that matches every project; this only collects the three claims and the
40610
+ // vault scope. --vault has no org-wide default.
40611
+ case "trust": {
40612
+ requireAuth();
40613
+ const sub = positionals[0] ?? "ls";
40614
+ if (sub === "ls") {
40615
+ const trusts = (await client.listAgentOidcTrusts()).trusts.filter((t) => !t.disabled_at);
40616
+ const vaultsOf = (t) => {
40617
+ const ids = t.resource_scope?.vault_ids ?? [];
40618
+ return ids.length ? ids.map((id) => `${t.resource_scope_names?.[id] ?? "vault"} (${id})`).join(", ") : "all vaults the owner can reach";
40619
+ };
40620
+ show(
40621
+ asJson ? trusts : trusts.map((t) => ({ id: t.id, issuer: t.issuer, audience: t.audience, subject: t.subject_pattern, vaults: vaultsOf(t), created: t.created_at })),
40622
+ ["id", "issuer", "audience", "subject", "vaults", "created"]
40623
+ );
40624
+ } else if (sub === "add") {
40625
+ const need = (k) => typeof flags[k] === "string" && String(flags[k]).trim() ? String(flags[k]).trim() : die(`--${k} is required; see \`kt trust --help\``);
40626
+ const issuer = need("issuer"), audience = need("audience"), subject = need("subject");
40627
+ const vaultIds = repeatedFlag(process.argv.slice(2), "vault").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
40628
+ if (!vaultIds.length) return die("--vault <id> is required (repeat it for more): a trust reaches only the vaults named, never the whole organization");
40629
+ const scopes = (typeof flags.scopes === "string" ? flags.scopes : "vault.list,vault.use").split(",").map((x) => x.trim()).filter(Boolean);
40630
+ const egress = String(flags.egress ?? "lan");
40631
+ if (egress !== "lan" && egress !== "cloud") return die("--egress must be lan or cloud");
40632
+ const trust = await withStepUp(client, () => client.createAgentOidcTrust({
40633
+ name: typeof flags.name === "string" && flags.name.trim() ? flags.name.trim() : `ci ${subject}`.slice(0, 120),
40634
+ issuer,
40635
+ audience,
40636
+ subject_pattern: subject,
40637
+ scopes,
40638
+ ...flags.ttl !== void 0 ? { ttl_minutes: Number(flags.ttl) } : {},
40639
+ egress_class: egress,
40640
+ resource_scope: { vault_ids: vaultIds }
40641
+ }));
40642
+ if (asJson) return console.log(JSON.stringify(trust, null, 2));
40643
+ console.log(`trust ${trust.id} created (egress: ${egress})`);
40644
+ console.log(`use it in the pipeline: kt ci-login --trust ${trust.id}`);
40645
+ } else if (sub === "rm" && positionals[1]) {
40646
+ await client.deleteAgentOidcTrust(positionals[1]);
40647
+ console.log(`trust ${positionals[1]} revoked; no new exchange is accepted. Agents already issued under it run out their own lifetime.`);
40648
+ } else {
40649
+ die("usage: kt trust add --issuer <url> --audience <aud> --subject <pattern> --vault <id> | kt trust ls | kt trust rm <id>");
40650
+ }
40651
+ break;
40652
+ }
40653
+ // #3561: only the ordered fallback list lives here; the primary base is set by `kt login`.
40654
+ case "config": {
40655
+ const [sub, key, ...vals] = positionals;
40656
+ if (key !== "base-fallback" || sub !== "set" && sub !== "get" && sub !== "unset") {
40657
+ return die("usage: kt config set base-fallback <url>[,<url>...] [--allow-insecure-lan] | kt config get base-fallback | kt config unset base-fallback");
40658
+ }
40659
+ const saved = storedBase();
40660
+ if (!saved) return die("no saved server to attach a fallback to; run `kt --base <url> login` first");
40661
+ if (sub === "set") {
40662
+ const urls = splitBases(vals);
40663
+ if (!urls.length) return die("give at least one fallback base URL");
40664
+ const entries = urls.map((url) => ({ url, ...flags["allow-insecure-lan"] === true && url.startsWith("http:") ? { insecure: true } : {} }));
40665
+ for (const f of entries) {
40666
+ const why2 = fallbackRefusal(f);
40667
+ if (why2) return die(`error: ${why2}`);
40668
+ if (norm(f.url) === norm(saved)) return die(`error: ${f.url} is the primary server`);
40669
+ }
40670
+ saveCfg(saved, entries);
40671
+ } else if (sub === "unset") {
40672
+ saveCfg(saved, []);
40673
+ }
40674
+ const now = readFallbacks(CONFIG, saved);
40675
+ if (asJson) return console.log(JSON.stringify({ base: saved, fallbacks: now }, null, 2));
40676
+ console.log(`primary ${saved}`);
40677
+ now.forEach((f, i) => console.log(`fallback ${i + 1} ${f.url}${f.insecure ? " (plain http, opted in)" : ""}`));
40678
+ if (!now.length) console.log("no fallback bases");
40679
+ break;
40680
+ }
39689
40681
  // #674: exchange this CI job's own OIDC token for a short-lived agent principal. No Kutrion
39690
40682
  // secret is stored in the pipeline — the job token is minted per run by the CI provider.
39691
40683
  case "ci-login": {
@@ -39901,7 +40893,15 @@ ${snippetPreview(s)}
39901
40893
  const connection_id = String(flags.conn ?? ""), credential_id = String(flags.cred ?? "");
39902
40894
  if (!connection_id || !credential_id) return die("need --conn <id> --cred <id>");
39903
40895
  const input = { connection_id, credential_id, service, target: flags.target ? String(flags.target) : void 0 };
39904
- show(sub === "plan" ? await client.planRotation(input) : await client.executeRotation(input));
40896
+ if (sub === "plan") show(await client.planRotation(input));
40897
+ else {
40898
+ try {
40899
+ show(await client.executeRotation(input));
40900
+ } catch (e) {
40901
+ if (isStepUpRequired(e)) console.error("requires_step_up: true (as `kt rotation plan` reports); run `kt step-up`, then re-run");
40902
+ throw e;
40903
+ }
40904
+ }
39905
40905
  } else die("usage: kt rotation services | history <credId> | plan|run --conn <id> --cred <id> --service <svc> [--target ..]");
39906
40906
  break;
39907
40907
  }
@@ -39973,8 +40973,9 @@ ${owed}`);
39973
40973
  case "device":
39974
40974
  requireAuth();
39975
40975
  if (positionals[0] === "rm" && positionals[1]) {
39976
- await client.revokeDevice(positionals[1]);
40976
+ const r = await client.revokeDevice(positionals[1]);
39977
40977
  console.log("revoked");
40978
+ if (r.agents_ended) console.log(`this ended ${r.agents_ended} agent${r.agents_ended === 1 ? "" : "s"}: ${(r.agents ?? []).map((a) => printable(String(a.name ?? ""))).join(", ")}`);
39978
40979
  } else die("usage: kt device rm <deviceId>");
39979
40980
  break;
39980
40981
  // --- vault share ---
@@ -40037,7 +41038,7 @@ ${r.url}`);
40037
41038
  } else if (sub === "import") {
40038
41039
  const vaultId = positionals[1], file = positionals[2];
40039
41040
  if (!vaultId || !file) return die(CONN_IMPORT_USAGE);
40040
- const parsed = await client.parseConnImport((0, import_fs5.readFileSync)(file, "utf8"), {
41041
+ const parsed = await client.parseConnImport((0, import_fs7.readFileSync)(file, "utf8"), {
40041
41042
  provider: flags.provider ? String(flags.provider) : void 0,
40042
41043
  filename: file.split("/").pop()
40043
41044
  });
@@ -40167,7 +41168,7 @@ ${ENROLL_USAGE}`);
40167
41168
  const confirms = parseConfirms(process.argv.slice(2));
40168
41169
  if (!(confirms instanceof Map)) return die(confirms.error);
40169
41170
  if (flags.key === true || flags["known-hosts"] === true || flags.vault === true) return die(ENROLL_USAGE);
40170
- const knownHosts = typeof flags["known-hosts"] === "string" ? (0, import_fs5.readFileSync)(flags["known-hosts"], "utf8") : void 0;
41171
+ const knownHosts = typeof flags["known-hosts"] === "string" ? (0, import_fs7.readFileSync)(flags["known-hosts"], "utf8") : void 0;
40171
41172
  const fallback = flags.fallback === true;
40172
41173
  const vaultId = typeof flags.vault === "string" ? flags.vault : void 0;
40173
41174
  const timeoutSec = intFlag("timeout") ?? 1800;
@@ -40187,7 +41188,7 @@ ${r.label} ${r.host.observedFingerprint}`);
40187
41188
  if (!chosen.length) return die(ENROLL_COPY.noneConfirmed);
40188
41189
  let bootstrap;
40189
41190
  if (typeof flags.key === "string") {
40190
- const privateKey = (0, import_fs5.readFileSync)(flags.key, "utf8");
41191
+ const privateKey = (0, import_fs7.readFileSync)(flags.key, "utf8");
40191
41192
  let passphrase;
40192
41193
  if (isEncryptedKey(privateKey)) {
40193
41194
  if (flags["password-stdin"] === true) passphrase = await readStdin();
@@ -40380,14 +41381,15 @@ ${BROKER_CA_USAGE}`);
40380
41381
  break;
40381
41382
  case "import-providers":
40382
41383
  requireAuth();
40383
- show((await client.importProviders()).providers, ["id", "name", "format", "ext"]);
41384
+ show((await client.importProviders()).providers.map((p) => ({ ...p, name: p.label })), ["id", "name", "format", "ext"]);
40384
41385
  break;
40385
41386
  case "import": {
40386
41387
  requireAuth();
40387
41388
  const vaultId = positionals[0];
40388
41389
  const file = positionals[1];
40389
41390
  if (!vaultId || !file) die("usage: kt import <vaultId> <file> [--provider <name>] (see: kt import-providers)");
40390
- const content = (0, import_fs5.readFileSync)(file, "utf8");
41391
+ const content = (0, import_fs7.readFileSync)(file, "utf8");
41392
+ if ((await vaultCustody(vaultId)).private) return die(PRIVATE_IMPORT_REFUSED);
40391
41393
  const parsed = await client.parseImport(content, { provider: flags.provider ? String(flags.provider) : void 0, filename: file.split("/").pop() });
40392
41394
  for (const w of parsed.warnings) console.error("warning:", w);
40393
41395
  const items = parsed.items.filter((it) => it.name && it.value).map((it) => ({ ...it, name: it.name.slice(0, 255) }));
@@ -40466,6 +41468,19 @@ Add this to your authenticator, then run: kt mfa verify --code <code>`);
40466
41468
  }
40467
41469
  break;
40468
41470
  }
41471
+ // The adaptive gate's step-up is a per-session proof (server lib/step-up.ts), so one code here clears
41472
+ // the refusal for every command this session runs until it lapses. The code never rides on argv.
41473
+ case "step-up": {
41474
+ requireAuth();
41475
+ if (positionals.length || flags.code !== void 0) die("usage: kt step-up (the authenticator code is asked for, or read from stdin)");
41476
+ const status = await client.mfaStatus();
41477
+ if (status.methods.totp !== true) die("this account has no authenticator app to step up with; enrol one with `kt mfa setup`, then `kt mfa verify --code <code>`, then run `kt step-up`");
41478
+ const code = await prompt("authenticator code: ", true);
41479
+ if (!code) die("no authenticator code given");
41480
+ await client.stepUpTotp(code);
41481
+ console.log("step-up verified; re-run the refused command within a few minutes");
41482
+ break;
41483
+ }
40469
41484
  case "passkeys":
40470
41485
  requireAuth();
40471
41486
  show((await client.listPasskeys()).passkeys, ["id", "label", "kind", "created_at", "last_used"]);