@kutrion/cli 1.2.12 → 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 +20 -1
  2. package/dist/index.js +1709 -462
  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
 
@@ -24054,6 +24054,152 @@ function auditResultLabel(raw) {
24054
24054
  return humanizeAction(raw);
24055
24055
  }
24056
24056
 
24057
+ // ../packages/shared/src/refresh-refusal-copy.ts
24058
+ init_define_KT_HUB_FILES();
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
+ }
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}`;
24168
+ };
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) {
24178
+ if (typeof code !== "string" || !Object.prototype.hasOwnProperty.call(REFRESH_REFUSAL_COPY, code)) return null;
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 });
24201
+ }
24202
+
24057
24203
  // ../packages/shared/src/client.ts
24058
24204
  init_define_KT_HUB_FILES();
24059
24205
 
@@ -27621,6 +27767,9 @@ var ApiError = class extends Error {
27621
27767
  this.headers = headers;
27622
27768
  this.name = "ApiError";
27623
27769
  }
27770
+ // #3445: the `error` code /auth/refresh gave when it ended the session behind this 401, set only by
27771
+ // the SDK's own refresh-then-replay path. `body` and `message` stay the original request's.
27772
+ refreshCode;
27624
27773
  // #2747: the error class slug — the server's `error` code, or the code the SDK minted itself — or
27625
27774
  // null when the reply carried none (the message is then a status phrase, not a code).
27626
27775
  get slug() {
@@ -27803,9 +27952,13 @@ async function withTimeout(inner, ms, input, init) {
27803
27952
  var REFRESH_BODY_MS = 3e3;
27804
27953
  var REFRESH_STORE_ATTEMPTS = 3;
27805
27954
  var REFRESH_STORE_TIMEOUT_MS = 1e4;
27955
+ var SIGNATURE_REQUIRED_COOLDOWN_MS = 3e4;
27806
27956
  function isRefreshSignatureRefusal(body) {
27807
27957
  const error = body?.error;
27808
- 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";
27809
27962
  }
27810
27963
  async function readBodyWithin(res, ms) {
27811
27964
  let timer;
@@ -27964,6 +28117,8 @@ var KutrionClient = class {
27964
28117
  // `next` is presented in its place: `spent` is already rotated, and presenting it reads as reuse.
27965
28118
  heldRefresh = null;
27966
28119
  refreshing = null;
28120
+ // performance.now() time, so a wall-clock step cannot lengthen or cut the cooldown.
28121
+ signatureRequiredUntil = 0;
27967
28122
  // VT-405 (#153): per-session key for decrypting sealed response payloads. Agreed lazily over ECDH
27968
28123
  // and held in memory only (re-agreed on demand after a reload / worker restart).
27969
28124
  sessionKey = null;
@@ -28043,6 +28198,7 @@ var KutrionClient = class {
28043
28198
  async tryRefresh() {
28044
28199
  const rt = this.presentableRefreshToken();
28045
28200
  if (!rt) return { outcome: "no_token" };
28201
+ if (!this.refreshing && performance.now() < this.signatureRequiredUntil) return { outcome: "indeterminate", status: 401 };
28046
28202
  if (!this.refreshing) {
28047
28203
  this.refreshing = (async () => {
28048
28204
  try {
@@ -28053,15 +28209,23 @@ var KutrionClient = class {
28053
28209
  });
28054
28210
  const { body: signed, signerThrew } = await this.signedRefreshBody(rt);
28055
28211
  let res = await post(signed);
28056
- if (res.status === 401 && signed.signature && isRefreshSignatureRefusal(await readBodyWithin(res, REFRESH_BODY_MS))) {
28057
- res = await post(this.refreshBody(rt));
28058
- if (res.ok) this.refreshSigningRefused = true;
28212
+ let refusalBody;
28213
+ if (res.status === 401 && signed.signature) {
28214
+ refusalBody = await readBodyWithin(res, REFRESH_BODY_MS);
28215
+ if (isRefreshSignatureRefusal(refusalBody)) {
28216
+ res = await post(this.refreshBody(rt));
28217
+ refusalBody = void 0;
28218
+ if (res.ok) this.refreshSigningRefused = true;
28219
+ }
28059
28220
  }
28060
28221
  if (res.status === 401) {
28061
- if (signerThrew && !signed.signature && isRefreshSignatureRefusal(await readBodyWithin(res, REFRESH_BODY_MS))) {
28222
+ if (refusalBody === void 0) refusalBody = await readBodyWithin(res, REFRESH_BODY_MS);
28223
+ if (signerThrew && !signed.signature && isRefreshSignatureRefusal(refusalBody) || isSignatureRequired(refusalBody)) {
28224
+ if (!signerThrew && isSignatureRequired(refusalBody)) this.signatureRequiredUntil = performance.now() + SIGNATURE_REQUIRED_COOLDOWN_MS;
28062
28225
  return { outcome: "indeterminate", status: 401 };
28063
28226
  }
28064
- return { outcome: "rejected", status: 401 };
28227
+ const code = refusalBody?.error;
28228
+ return typeof code === "string" && code ? { outcome: "rejected", status: 401, code } : { outcome: "rejected", status: 401 };
28065
28229
  }
28066
28230
  if (!res.ok) {
28067
28231
  const body = res.status === 403 ? await readBodyWithin(res, REFRESH_BODY_MS) : void 0;
@@ -28147,6 +28311,7 @@ var KutrionClient = class {
28147
28311
  data = JSON.parse(await decryptEnvelope(this.sessionKey, data.enc));
28148
28312
  }
28149
28313
  if (!res.ok) {
28314
+ let refreshCode;
28150
28315
  if (res.status === 401 && auth) {
28151
28316
  if (_retry) {
28152
28317
  this.onUnauthorized?.();
@@ -28156,6 +28321,9 @@ var KutrionClient = class {
28156
28321
  case "refreshed":
28157
28322
  return this.req(method, path, body, auth, true);
28158
28323
  case "rejected":
28324
+ refreshCode = r.code;
28325
+ this.onUnauthorized?.(r.code);
28326
+ break;
28159
28327
  case "no_token":
28160
28328
  this.onUnauthorized?.();
28161
28329
  break;
@@ -28170,7 +28338,9 @@ var KutrionClient = class {
28170
28338
  }
28171
28339
  }
28172
28340
  }
28173
- throw new ApiError(res.status, apiErrorMessage(res.status, data), data, res.headers);
28341
+ const err = new ApiError(res.status, apiErrorMessage(res.status, data), data, res.headers);
28342
+ if (refreshCode) err.refreshCode = refreshCode;
28343
+ throw err;
28174
28344
  }
28175
28345
  return data;
28176
28346
  }
@@ -28420,7 +28590,7 @@ var KutrionClient = class {
28420
28590
  // #2578: clears a factor's pending-approval mark. A step-up-pending session is refused with 403
28421
28591
  // step_up_required; `approved: false` means the factor was already approved.
28422
28592
  approveFactor(kind, id) {
28423
- return this.req("POST", `/auth/factors/${kind}/${encodeURIComponent(id)}/approve`);
28593
+ return this.req("POST", `/auth/factors/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/approve`);
28424
28594
  }
28425
28595
  mfaGenerateBackupCodes() {
28426
28596
  return this.req("POST", "/auth/mfa/backup-codes/generate");
@@ -28440,14 +28610,17 @@ var KutrionClient = class {
28440
28610
  // losing its keys; the server revokes that row if it is the caller's and ignores it otherwise.
28441
28611
  async registerDevice(input) {
28442
28612
  const reg = await this.req("POST", "/auth/devices/register", input);
28443
- if (reg.refresh_key_bound) this.refreshSigningRefused = false;
28613
+ if (reg.refresh_key_bound) {
28614
+ this.refreshSigningRefused = false;
28615
+ this.signatureRequiredUntil = 0;
28616
+ }
28444
28617
  return reg;
28445
28618
  }
28446
28619
  // #301: mint a second refresh-token family for an existing device, owned by a companion process
28447
28620
  // (the iOS AutoFill extension) so it rotates independently of the app and never trips the app's
28448
28621
  // reuse-detection. The extension persists its own rotations; the app must not share its token.
28449
28622
  provisionCompanionSession(deviceId) {
28450
- return this.req("POST", `/auth/devices/${deviceId}/companion`, {});
28623
+ return this.req("POST", `/auth/devices/${encodeURIComponent(deviceId)}/companion`, {});
28451
28624
  }
28452
28625
  // #768: `drain_limits` is what each device's `drain.state` may and may not be said to prove. The
28453
28626
  // server ships the sentence so three clients cannot each invent their own; optional here only
@@ -28456,7 +28629,7 @@ var KutrionClient = class {
28456
28629
  return this.req("GET", "/auth/devices");
28457
28630
  }
28458
28631
  revokeDevice(id) {
28459
- return this.req("DELETE", `/auth/devices/${id}`, void 0);
28632
+ return this.req("DELETE", `/auth/devices/${encodeURIComponent(id)}`, void 0);
28460
28633
  }
28461
28634
  // Rotate tokens explicitly (req() also does this transparently on 401).
28462
28635
  async refreshTokens(refreshToken) {
@@ -28464,8 +28637,11 @@ var KutrionClient = class {
28464
28637
  try {
28465
28638
  return await this.req("POST", "/auth/refresh", body, false);
28466
28639
  } catch (e) {
28467
- if (signerThrew && !body.signature && e instanceof ApiError && e.status === 401 && isRefreshSignatureRefusal(e.body)) {
28468
- 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
+ }
28469
28645
  }
28470
28646
  throw e;
28471
28647
  }
@@ -28485,7 +28661,10 @@ var KutrionClient = class {
28485
28661
  refreshKey ? { device_id: deviceId, signature, refresh_key: refreshKey } : { device_id: deviceId, signature },
28486
28662
  false
28487
28663
  );
28488
- if (tokens.refresh_key_bound) this.refreshSigningRefused = false;
28664
+ if (tokens.refresh_key_bound) {
28665
+ this.refreshSigningRefused = false;
28666
+ this.signatureRequiredUntil = 0;
28667
+ }
28489
28668
  return tokens;
28490
28669
  }
28491
28670
  // --- local vault cache: keys (#709, epic #708) ---
@@ -28493,7 +28672,7 @@ var KutrionClient = class {
28493
28672
  // nowhere else afterwards, so a caller that drops it must either re-issue (same key while it lives)
28494
28673
  // or rotate — and rotating deliberately kills whatever the old key was protecting.
28495
28674
  issueCacheKey(deviceId, opts2) {
28496
- return this.req("POST", `/auth/devices/${deviceId}/cache-key`, { rotate: opts2?.rotate === true });
28675
+ return this.req("POST", `/auth/devices/${encodeURIComponent(deviceId)}/cache-key`, { rotate: opts2?.rotate === true });
28497
28676
  }
28498
28677
  // #712: drain this device's local reveal journal. The batch is MAC'd under the cache KEK; a broken
28499
28678
  // chain answers 409 with the head the server holds, never a silent success.
@@ -28508,10 +28687,10 @@ var KutrionClient = class {
28508
28687
  return this.req("POST", "/audit/journal-discarded", input);
28509
28688
  }
28510
28689
  cacheKeyStatus(deviceId) {
28511
- return this.req("GET", `/auth/devices/${deviceId}/cache-key`);
28690
+ return this.req("GET", `/auth/devices/${encodeURIComponent(deviceId)}/cache-key`);
28512
28691
  }
28513
28692
  revokeCacheKey(deviceId) {
28514
- return this.req("DELETE", `/auth/devices/${deviceId}/cache-key`);
28693
+ return this.req("DELETE", `/auth/devices/${encodeURIComponent(deviceId)}/cache-key`);
28515
28694
  }
28516
28695
  listCacheKeys() {
28517
28696
  return this.req("GET", "/auth/cache-keys");
@@ -28532,21 +28711,24 @@ var KutrionClient = class {
28532
28711
  return this.req("GET", "/auth/webauthn");
28533
28712
  }
28534
28713
  deletePasskey(id) {
28535
- return this.req("DELETE", `/auth/webauthn/${id}`);
28714
+ return this.req("DELETE", `/auth/webauthn/${encodeURIComponent(id)}`);
28715
+ }
28716
+ renamePasskey(id, label) {
28717
+ return this.req("PATCH", `/auth/webauthn/${encodeURIComponent(id)}`, { label });
28536
28718
  }
28537
28719
  // VT-120: linked sign-in methods (SSO/social identities) + set a first password for social-only accounts.
28538
28720
  listIdentities() {
28539
28721
  return this.req("GET", "/auth/identities");
28540
28722
  }
28541
28723
  unlinkIdentity(id) {
28542
- return this.req("DELETE", `/auth/identities/${id}`);
28724
+ return this.req("DELETE", `/auth/identities/${encodeURIComponent(id)}`);
28543
28725
  }
28544
28726
  // #2678: begin linking a platform provider. The browser then navigates to `url`; the callback hands
28545
28727
  // back a handle that completeIdentityLink attaches from this same session. #2847: a native app passes
28546
28728
  // its registered client and the challenge for a verifier it keeps, then completes with that verifier.
28547
28729
  startIdentityLink(ref, binding, currentPassword) {
28548
28730
  const how = typeof binding === "string" ? { cb: binding } : { native: true, native_client: binding.nativeClient, native_challenge: binding.challenge };
28549
- return this.req("POST", `/auth/sso/${ref}/link`, { ...how, ...currentPassword ? { current_password: currentPassword } : {} });
28731
+ return this.req("POST", `/auth/sso/${encodeURIComponent(ref)}/link`, { ...how, ...currentPassword ? { current_password: currentPassword } : {} });
28550
28732
  }
28551
28733
  completeIdentityLink(handle, verifier) {
28552
28734
  return this.req("POST", "/auth/sso/link/complete", { handle, ...verifier ? { verifier } : {} });
@@ -28587,15 +28769,15 @@ var KutrionClient = class {
28587
28769
  // Works for both OIDC and SAML providers — /start dispatches on the provider's protocol. `ref`
28588
28770
  // is the provider id (preferred, unambiguous across orgs) or a platform provider's slug.
28589
28771
  ssoStartUrl(ref) {
28590
- return `${this.baseUrl}/auth/sso/${ref}/start`;
28772
+ return `${this.baseUrl}/auth/sso/${encodeURIComponent(ref)}/start`;
28591
28773
  }
28592
28774
  // SP metadata URL to hand a SAML IdP (entityID + ACS endpoint). Keyed by provider id.
28593
28775
  samlMetadataUrl(ref) {
28594
- return `${this.baseUrl}/auth/sso/${ref}/saml/metadata`;
28776
+ return `${this.baseUrl}/auth/sso/${encodeURIComponent(ref)}/saml/metadata`;
28595
28777
  }
28596
28778
  // OIDC redirect URI to register at the IdP, keyed by provider id (per-org safe).
28597
28779
  ssoCallbackUrl(ref) {
28598
- return `${this.baseUrl}/auth/sso/${ref}/callback`;
28780
+ return `${this.baseUrl}/auth/sso/${encodeURIComponent(ref)}/callback`;
28599
28781
  }
28600
28782
  // --- SSO provider admin (MT-3, org admin/owner only; secrets are write-only) ---
28601
28783
  listSsoProvidersAdmin() {
@@ -28686,10 +28868,10 @@ var KutrionClient = class {
28686
28868
  return this.req("POST", "/workspaces", input);
28687
28869
  }
28688
28870
  updateWorkspace(id, input) {
28689
- return this.req("PUT", `/workspaces/${id}`, input);
28871
+ return this.req("PUT", `/workspaces/${encodeURIComponent(id)}`, input);
28690
28872
  }
28691
28873
  deleteWorkspace(id) {
28692
- return this.req("DELETE", `/workspaces/${id}`);
28874
+ return this.req("DELETE", `/workspaces/${encodeURIComponent(id)}`);
28693
28875
  }
28694
28876
  // --- relay (#458 vault-synced API-client documents; #464 team grants) ---
28695
28877
  listRelayDocuments() {
@@ -28705,7 +28887,7 @@ var KutrionClient = class {
28705
28887
  return this.req("POST", `/relay/documents/${encodeURIComponent(clientId)}/grants`, { team_id, role });
28706
28888
  }
28707
28889
  relayUnshare(clientId, teamId) {
28708
- return this.req("DELETE", `/relay/documents/${encodeURIComponent(clientId)}/grants/${teamId}`);
28890
+ return this.req("DELETE", `/relay/documents/${encodeURIComponent(clientId)}/grants/${encodeURIComponent(teamId)}`);
28709
28891
  }
28710
28892
  // --- vault ---
28711
28893
  listVaults() {
@@ -28715,13 +28897,13 @@ var KutrionClient = class {
28715
28897
  return this.req("POST", "/vault/create", { name, team_id });
28716
28898
  }
28717
28899
  renameVault(vaultId, name) {
28718
- return this.req("PATCH", `/vault/${vaultId}`, { name });
28900
+ return this.req("PATCH", `/vault/${encodeURIComponent(vaultId)}`, { name });
28719
28901
  }
28720
28902
  deleteVault(vaultId) {
28721
- return this.req("DELETE", `/vault/${vaultId}`);
28903
+ return this.req("DELETE", `/vault/${encodeURIComponent(vaultId)}`);
28722
28904
  }
28723
28905
  listCredentials(vaultId) {
28724
- return this.req("GET", `/vault/${vaultId}/credentials`);
28906
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials`);
28725
28907
  }
28726
28908
  // #1122: listVaults + every listCredentials in one round trip, with the same permissions gating.
28727
28909
  // Metadata only — revealing a secret is still the audited per-credential call.
@@ -28747,19 +28929,19 @@ var KutrionClient = class {
28747
28929
  return this.req("GET", `/vault/host-matches?host=${encodeURIComponent(host2)}`);
28748
28930
  }
28749
28931
  getCredential(vaultId, cid) {
28750
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}`);
28932
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}`);
28751
28933
  }
28752
28934
  // #717: `mutation_id` and `base_updated_at` are what a replayed offline write adds — the first
28753
28935
  // makes a retry a no-op, the second lets the server tell an edit from an overwrite.
28754
28936
  storeCredential(vaultId, input) {
28755
- return this.req("POST", `/vault/${vaultId}/credentials`, input);
28937
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/credentials`, input);
28756
28938
  }
28757
28939
  updateCredential(vaultId, cid, input) {
28758
- return this.req("PATCH", `/vault/${vaultId}/credentials/${cid}`, input);
28940
+ return this.req("PATCH", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}`, input);
28759
28941
  }
28760
28942
  deleteCredential(vaultId, cid, opts2) {
28761
28943
  const q = opts2?.mutation_id ? `?mutation_id=${encodeURIComponent(opts2.mutation_id)}` : "";
28762
- return this.req("DELETE", `/vault/${vaultId}/credentials/${cid}${q}`);
28944
+ return this.req("DELETE", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}` + q);
28763
28945
  }
28764
28946
  // #2622: snippets. A brokered shell runs one over its socket (snippetFrame); useSnippet is for a
28765
28947
  // terminal the server does not broker, and records the use the same way.
@@ -28787,23 +28969,23 @@ var KutrionClient = class {
28787
28969
  return this.req("GET", `/vault/conflicts${opts2?.open === false ? "?open=false" : ""}`);
28788
28970
  }
28789
28971
  acknowledgeConflict(id) {
28790
- return this.req("POST", `/vault/conflicts/${id}/ack`);
28972
+ return this.req("POST", `/vault/conflicts/${encodeURIComponent(id)}/ack`);
28791
28973
  }
28792
28974
  conflictPrevious(id) {
28793
- return this.req("GET", `/vault/conflicts/${id}/previous`);
28975
+ return this.req("GET", `/vault/conflicts/${encodeURIComponent(id)}/previous`);
28794
28976
  }
28795
28977
  // VT-487: group RBAC on a vault (collection) + item folders.
28796
28978
  listVaultGrants(vaultId) {
28797
- return this.req("GET", `/vault/${vaultId}/grants`);
28979
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/grants`);
28798
28980
  }
28799
28981
  grantVaultToTeam(vaultId, input) {
28800
- return this.req("POST", `/vault/${vaultId}/grants`, input);
28982
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/grants`, input);
28801
28983
  }
28802
28984
  revokeVaultGrant(vaultId, grantId) {
28803
- return this.req("DELETE", `/vault/${vaultId}/grants/${grantId}`);
28985
+ return this.req("DELETE", `/vault/${encodeURIComponent(vaultId)}/grants/${encodeURIComponent(grantId)}`);
28804
28986
  }
28805
28987
  listVaultFolders(vaultId) {
28806
- return this.req("GET", `/vault/${vaultId}/folders`);
28988
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/folders`);
28807
28989
  }
28808
28990
  // #1169: the credential's destination allow-list — where its secret may be presented. The routes
28809
28991
  // live in their own module mounted on /vault (server/src/index.ts:310), so the paths hang off the
@@ -28820,36 +29002,45 @@ var KutrionClient = class {
28820
29002
  // No write response carries the three list-only fields; re-list rather than deriving them.
28821
29003
  // #2634: who revealed or used one credential. Vault owners and admins only; never carries the value.
28822
29004
  credentialProvenance(vaultId, cid) {
28823
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}/provenance`);
29005
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/provenance`);
28824
29006
  }
28825
29007
  // #3074: password history. The list carries no values; revealing one is a gated read like getCredential.
28826
29008
  listCredentialHistory(vaultId, cid) {
28827
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}/history`);
29009
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/history`);
28828
29010
  }
28829
29011
  revealCredentialHistory(vaultId, cid, hid) {
28830
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}/history/${hid}`);
29012
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/history/${encodeURIComponent(hid)}`);
28831
29013
  }
28832
29014
  restoreCredentialHistory(vaultId, cid, hid) {
28833
- return this.req("POST", `/vault/${vaultId}/credentials/${cid}/history/${hid}/restore`);
29015
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/history/${encodeURIComponent(hid)}/restore`);
28834
29016
  }
28835
29017
  listCredentialDestinations(vaultId, cid) {
28836
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}/destinations`);
29018
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/destinations`);
28837
29019
  }
28838
29020
  // `port` omitted means this protocol's default, resolved and stored server-side — NOT a wildcard, and
28839
29021
  // not something to fill in here: a client-supplied default would come back as a chosen port.
28840
29022
  //
28841
- // `secret` is the credential's current plaintext and is required by design. Write on the vault gates
28842
- // who may ask; the secret is what makes a repoint cost as much as knowing the credential, which is
28843
- // the whole point of the control. Injection-only credentials cannot satisfy it and answer 403
28844
- // `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`.
28845
29032
  //
28846
29033
  // #1170: `domain` is RDP-only and omitting it means the credential is presented with NO domain — not
28847
29034
  // "any domain". It is the third member of the RDP destination tuple because it selects which
28848
29035
  // directory authenticates the credential, so a binding that omits it does not authorise one that
28849
29036
  // names it. Sent on any other protocol it answers 400 `domain_not_supported`; unexpressible values
28850
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.
28851
29042
  addCredentialDestination(vaultId, cid, input) {
28852
- return this.req("POST", `/vault/${vaultId}/credentials/${cid}/destinations`, input);
29043
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/destinations`, input);
28853
29044
  }
28854
29045
  // Gated on the secret for the same reason adding is, but NOT because removal widens — #1281 reversed
28855
29046
  // that. Emptying a protocol while rows survive elsewhere leaves the credential claimed, so that
@@ -28861,7 +29052,7 @@ var KutrionClient = class {
28861
29052
  removeCredentialDestination(vaultId, cid, destinationId, secret) {
28862
29053
  return this.req(
28863
29054
  "DELETE",
28864
- `/vault/${vaultId}/credentials/${cid}/destinations/${destinationId}`,
29055
+ `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/destinations/${encodeURIComponent(destinationId)}`,
28865
29056
  { secret }
28866
29057
  );
28867
29058
  }
@@ -28901,20 +29092,20 @@ var KutrionClient = class {
28901
29092
  return this.req("POST", "/vault/breach-alerts/scan");
28902
29093
  }
28903
29094
  updateBreachAlert(id, status) {
28904
- return this.req("PATCH", `/vault/breach-alerts/${id}`, { status });
29095
+ return this.req("PATCH", `/vault/breach-alerts/${encodeURIComponent(id)}`, { status });
28905
29096
  }
28906
29097
  setBreachMonitor(enabled) {
28907
29098
  return this.req("PUT", "/vault/breach-alerts/settings", { enabled });
28908
29099
  }
28909
29100
  importEnv(vaultId, name, dotenv) {
28910
- return this.req("POST", `/vault/${vaultId}/env-import`, { name, dotenv });
29101
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/env-import`, { name, dotenv });
28911
29102
  }
28912
29103
  readEnv(vaultId, cid) {
28913
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}/env`);
29104
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/env`);
28914
29105
  }
28915
29106
  // #96: live TOTP code for a TOTP credential (secret stays on the broker).
28916
29107
  getTotpCode(vaultId, cid) {
28917
- return this.req("GET", `/vault/${vaultId}/credentials/${cid}/totp`);
29108
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/totp`);
28918
29109
  }
28919
29110
  // --- passkey provider (VT-405g): Kutrion as a roaming WebAuthn authenticator for OTHER sites
28920
29111
  // (distinct from listPasskeys/deletePasskey above, which are passkeys for logging into Kutrion). ---
@@ -28935,7 +29126,7 @@ var KutrionClient = class {
28935
29126
  return this.req("POST", "/share", { credential_id: credentialId, ...opts2 });
28936
29127
  }
28937
29128
  redeemShare(token) {
28938
- return this.req("GET", `/share/${token}`, void 0, false);
29129
+ return this.req("GET", `/share/${encodeURIComponent(token)}`, void 0, false);
28939
29130
  }
28940
29131
  // #869: with a credential id, every link issued for that credential (needs read on it); without
28941
29132
  // one, only the links the caller issued.
@@ -28943,14 +29134,14 @@ var KutrionClient = class {
28943
29134
  return this.req("GET", `/share${credentialId ? `?credential_id=${encodeURIComponent(credentialId)}` : ""}`);
28944
29135
  }
28945
29136
  revokeShare(id) {
28946
- return this.req("DELETE", `/share/${id}`);
29137
+ return this.req("DELETE", `/share/${encodeURIComponent(id)}`);
28947
29138
  }
28948
29139
  // --- import / export ---
28949
29140
  exportVault(vaultId) {
28950
- return this.req("GET", `/vault/${vaultId}/export`);
29141
+ return this.req("GET", `/vault/${encodeURIComponent(vaultId)}/export`);
28951
29142
  }
28952
29143
  importVault(vaultId, items) {
28953
- return this.req("POST", `/vault/${vaultId}/import`, { items });
29144
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/import`, { items });
28954
29145
  }
28955
29146
  importProviders() {
28956
29147
  return this.req("GET", "/vault/import/providers");
@@ -28988,7 +29179,7 @@ var KutrionClient = class {
28988
29179
  * a move's `enabled:false` after the caller's fields precisely so one request can never both repoint a
28989
29180
  * partner and turn it back on, and a client that batched them would be asking to be refused. */
28990
29181
  setAs2PartnerEnabled(id, enabled) {
28991
- return this.req("PATCH", `/as2/partners/${id}`, { enabled });
29182
+ return this.req("PATCH", `/as2/partners/${encodeURIComponent(id)}`, { enabled });
28992
29183
  }
28993
29184
  listAs2Messages() {
28994
29185
  return this.req("GET", "/as2/messages");
@@ -29001,33 +29192,33 @@ var KutrionClient = class {
29001
29192
  return this.req("POST", "/teams", { name });
29002
29193
  }
29003
29194
  listMembers(teamId) {
29004
- return this.req("GET", `/teams/${teamId}/members`);
29195
+ return this.req("GET", `/teams/${encodeURIComponent(teamId)}/members`);
29005
29196
  }
29006
29197
  addMember(teamId, email, role) {
29007
- return this.req("POST", `/teams/${teamId}/members`, { email, role });
29198
+ return this.req("POST", `/teams/${encodeURIComponent(teamId)}/members`, { email, role });
29008
29199
  }
29009
29200
  removeMember(teamId, userId) {
29010
- return this.req("DELETE", `/teams/${teamId}/members/${userId}`);
29201
+ return this.req("DELETE", `/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(userId)}`);
29011
29202
  }
29012
29203
  // #2775: org owner or admin only. 409 team_has_vaults or scim_managed.
29013
29204
  deleteTeam(teamId) {
29014
- return this.req("DELETE", `/teams/${teamId}`);
29205
+ return this.req("DELETE", `/teams/${encodeURIComponent(teamId)}`);
29015
29206
  }
29016
29207
  // --- team invitations (VT-102) ---
29017
29208
  inviteMember(teamId, email, role) {
29018
- return this.req("POST", `/teams/${teamId}/invitations`, { email, role });
29209
+ return this.req("POST", `/teams/${encodeURIComponent(teamId)}/invitations`, { email, role });
29019
29210
  }
29020
29211
  listInvitations(teamId) {
29021
- return this.req("GET", `/teams/${teamId}/invitations`);
29212
+ return this.req("GET", `/teams/${encodeURIComponent(teamId)}/invitations`);
29022
29213
  }
29023
29214
  revokeInvitation(teamId, invId) {
29024
- return this.req("DELETE", `/teams/${teamId}/invitations/${invId}`);
29215
+ return this.req("DELETE", `/teams/${encodeURIComponent(teamId)}/invitations/${encodeURIComponent(invId)}`);
29025
29216
  }
29026
29217
  invitePreview(token) {
29027
- return this.req("GET", `/teams/invitations/${token}`, void 0, false);
29218
+ return this.req("GET", `/teams/invitations/${encodeURIComponent(token)}`, void 0, false);
29028
29219
  }
29029
29220
  acceptInvitation(token) {
29030
- return this.req("POST", `/teams/invitations/${token}/accept`);
29221
+ return this.req("POST", `/teams/invitations/${encodeURIComponent(token)}/accept`);
29031
29222
  }
29032
29223
  // --- org onboarding (MT-4 #115): admin/owner-gated unless noted ---
29033
29224
  getOrg() {
@@ -29088,10 +29279,10 @@ var KutrionClient = class {
29088
29279
  return this.req("POST", "/emergency/contacts", input);
29089
29280
  }
29090
29281
  removeEmergencyContact(id) {
29091
- return this.req("DELETE", `/emergency/contacts/${id}`);
29282
+ return this.req("DELETE", `/emergency/contacts/${encodeURIComponent(id)}`);
29092
29283
  }
29093
29284
  denyEmergencyRequest(id) {
29094
- return this.req("POST", `/emergency/requests/${id}/deny`);
29285
+ return this.req("POST", `/emergency/requests/${encodeURIComponent(id)}/deny`);
29095
29286
  }
29096
29287
  listEmergencyGrants() {
29097
29288
  return this.req("GET", "/emergency/grants");
@@ -29100,10 +29291,10 @@ var KutrionClient = class {
29100
29291
  return this.req("POST", "/emergency/grants/accept", { token });
29101
29292
  }
29102
29293
  requestEmergencyAccess(grantId) {
29103
- return this.req("POST", `/emergency/grants/${grantId}/request`);
29294
+ return this.req("POST", `/emergency/grants/${encodeURIComponent(grantId)}/request`);
29104
29295
  }
29105
29296
  completeEmergencyAccess(requestId) {
29106
- return this.req("POST", `/emergency/requests/${requestId}/complete`);
29297
+ return this.req("POST", `/emergency/requests/${encodeURIComponent(requestId)}/complete`);
29107
29298
  }
29108
29299
  // VT-443: the user-facing announcement feed (filtered server-side by the org's tier/status).
29109
29300
  listAnnouncements() {
@@ -29193,7 +29384,7 @@ var KutrionClient = class {
29193
29384
  // #1959: org-admin surface grants for one member of the caller's own org. Distinct from
29194
29385
  // selfGrantSurface, which only ever acts on the caller.
29195
29386
  listUserSurfaceGrants(userId) {
29196
- return this.req("GET", `/surfaces/users/${userId}`);
29387
+ return this.req("GET", `/surfaces/users/${encodeURIComponent(userId)}`);
29197
29388
  }
29198
29389
  grantUserSurface(userId, surface, opts2) {
29199
29390
  return this.req("POST", "/surfaces/grants", {
@@ -29294,15 +29485,15 @@ var KutrionClient = class {
29294
29485
  return this.req("GET", "/org/members");
29295
29486
  }
29296
29487
  setOrgMemberRole(id, org_role) {
29297
- return this.req("PATCH", `/org/members/${id}`, { org_role });
29488
+ return this.req("PATCH", `/org/members/${encodeURIComponent(id)}`, { org_role });
29298
29489
  }
29299
29490
  removeOrgMember(id) {
29300
- return this.req("DELETE", `/org/members/${id}`);
29491
+ return this.req("DELETE", `/org/members/${encodeURIComponent(id)}`);
29301
29492
  }
29302
29493
  // VT-483: admin-mediated recovery — email a locked-out member a password-reset link/code. The admin
29303
29494
  // never sets or learns the new password.
29304
29495
  resetOrgMemberPassword(id) {
29305
- return this.req("POST", `/org/members/${id}/password-reset`);
29496
+ return this.req("POST", `/org/members/${encodeURIComponent(id)}/password-reset`);
29306
29497
  }
29307
29498
  // #1433: admin-mediated ACCOUNT RECOVERY, for a member who cannot sign in AT ALL. Distinct from the
29308
29499
  // password reset above, which refuses exactly the accounts that most need it: an SSO- or
@@ -29314,7 +29505,7 @@ var KutrionClient = class {
29314
29505
  // step_up_required), refuses a suspended member (409 member_inactive), and refuses a non-owner admin
29315
29506
  // who targets an owner (403 owner_recovery_requires_owner).
29316
29507
  recoverOrgMember(id) {
29317
- return this.req("POST", `/org/members/${id}/recover`);
29508
+ return this.req("POST", `/org/members/${encodeURIComponent(id)}/recover`);
29318
29509
  }
29319
29510
  inviteToOrg(email, org_role) {
29320
29511
  return this.req("POST", "/org/invitations", { email, org_role });
@@ -29323,13 +29514,13 @@ var KutrionClient = class {
29323
29514
  return this.req("GET", "/org/invitations");
29324
29515
  }
29325
29516
  revokeOrgInvitation(id) {
29326
- return this.req("DELETE", `/org/invitations/${id}`);
29517
+ return this.req("DELETE", `/org/invitations/${encodeURIComponent(id)}`);
29327
29518
  }
29328
29519
  orgInvitePreview(token) {
29329
- return this.req("GET", `/org/invitations/${token}`, void 0, false);
29520
+ return this.req("GET", `/org/invitations/${encodeURIComponent(token)}`, void 0, false);
29330
29521
  }
29331
29522
  async acceptOrgInvitation(token) {
29332
- const r = await this.req("POST", `/org/invitations/${token}/accept`);
29523
+ const r = await this.req("POST", `/org/invitations/${encodeURIComponent(token)}/accept`);
29333
29524
  this.onToken?.(r.token);
29334
29525
  return r;
29335
29526
  }
@@ -29342,18 +29533,18 @@ var KutrionClient = class {
29342
29533
  return this.req("POST", "/msp/clients", { client_email, role, ...expires_days ? { expires_days } : {} });
29343
29534
  }
29344
29535
  revokeMspClient(id) {
29345
- return this.req("POST", `/msp/clients/${id}/revoke`);
29536
+ return this.req("POST", `/msp/clients/${encodeURIComponent(id)}/revoke`);
29346
29537
  }
29347
29538
  // Mint a read-only "view as client" scoped session (#415c). The token is used as the active bearer;
29348
29539
  // the existing impersonation guard makes it read-only. Revoking the relationship kills it instantly.
29349
29540
  viewAsMspClient(id) {
29350
- return this.req("POST", `/msp/clients/${id}/view-as`);
29541
+ return this.req("POST", `/msp/clients/${encodeURIComponent(id)}/view-as`);
29351
29542
  }
29352
29543
  // #432: enter a WRITE-capable "act as client" session at the clamped effective role. Requires a fresh
29353
29544
  // step-up; writes are bounded by the deny-by-default server allowlist. Returns 403 step_up_required if
29354
29545
  // the operator has not recently verified a passkey/MFA, 409 read_only_relationship for a VIEWER ceiling.
29355
29546
  actAsMspClient(id, role = "EDITOR") {
29356
- return this.req("POST", `/msp/clients/${id}/act-as`, { role });
29547
+ return this.req("POST", `/msp/clients/${encodeURIComponent(id)}/act-as`, { role });
29357
29548
  }
29358
29549
  // #415d: the MSP's own access ledger across managed clients (sessions opened, last access).
29359
29550
  mspClientsActivity() {
@@ -29377,13 +29568,13 @@ var KutrionClient = class {
29377
29568
  return this.req("GET", "/msp/providers");
29378
29569
  }
29379
29570
  acceptMspProvider(id) {
29380
- return this.req("POST", `/msp/providers/${id}/accept`);
29571
+ return this.req("POST", `/msp/providers/${encodeURIComponent(id)}/accept`);
29381
29572
  }
29382
29573
  declineMspProvider(id) {
29383
- return this.req("POST", `/msp/providers/${id}/decline`);
29574
+ return this.req("POST", `/msp/providers/${encodeURIComponent(id)}/decline`);
29384
29575
  }
29385
29576
  revokeMspProvider(id) {
29386
- return this.req("POST", `/msp/providers/${id}/revoke`);
29577
+ return this.req("POST", `/msp/providers/${encodeURIComponent(id)}/revoke`);
29387
29578
  }
29388
29579
  // #434: what my providers did in my org (read-only-session opens etc.), for client-side transparency.
29389
29580
  mspProviderActivity() {
@@ -29417,13 +29608,13 @@ var KutrionClient = class {
29417
29608
  setMemberActive(memberId, active, reason) {
29418
29609
  return this.req(
29419
29610
  "PATCH",
29420
- `/org/members/${memberId}/active`,
29611
+ `/org/members/${encodeURIComponent(memberId)}/active`,
29421
29612
  { active, ...reason ? { reason } : {} }
29422
29613
  );
29423
29614
  }
29424
29615
  // Offboarding: kill every offline copy a member holds, from the admin side.
29425
29616
  revokeMemberCacheKeys(memberId) {
29426
- return this.req("DELETE", `/org/members/${memberId}/cache-keys`);
29617
+ return this.req("DELETE", `/org/members/${encodeURIComponent(memberId)}/cache-keys`);
29427
29618
  }
29428
29619
  // #815: the other half of that revoke — which devices came back and were told to wipe, and which
29429
29620
  // have not been seen since. Org-admin only.
@@ -29439,7 +29630,7 @@ var KutrionClient = class {
29439
29630
  return this.req("POST", "/org/equivalent-domains", { domains, note: note ?? null });
29440
29631
  }
29441
29632
  removeEquivalentDomains(id) {
29442
- return this.req("DELETE", `/org/equivalent-domains/${id}`);
29633
+ return this.req("DELETE", `/org/equivalent-domains/${encodeURIComponent(id)}`);
29443
29634
  }
29444
29635
  // #904: the caller's OWN remembered host bindings. Per user and per (credential, host) — never per
29445
29636
  // org, which would make phishing one member a lateral-movement primitive. Creation is screened
@@ -29449,10 +29640,10 @@ var KutrionClient = class {
29449
29640
  return this.req("GET", "/vault/site-bindings");
29450
29641
  }
29451
29642
  createSiteBinding(vaultId, cid, targetUrl, sourceUrl) {
29452
- return this.req("POST", `/vault/${vaultId}/credentials/${cid}/site-bindings`, { target_url: targetUrl, source_url: sourceUrl });
29643
+ return this.req("POST", `/vault/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(cid)}/site-bindings`, { target_url: targetUrl, source_url: sourceUrl });
29453
29644
  }
29454
29645
  revokeSiteBinding(id) {
29455
- return this.req("DELETE", `/vault/site-bindings/${id}`);
29646
+ return this.req("DELETE", `/vault/site-bindings/${encodeURIComponent(id)}`);
29456
29647
  }
29457
29648
  listOrgDomains() {
29458
29649
  return this.req("GET", "/org/domains");
@@ -29461,10 +29652,10 @@ var KutrionClient = class {
29461
29652
  return this.req("POST", "/org/domains", { domain });
29462
29653
  }
29463
29654
  verifyOrgDomain(id) {
29464
- return this.req("POST", `/org/domains/${id}/verify`);
29655
+ return this.req("POST", `/org/domains/${encodeURIComponent(id)}/verify`);
29465
29656
  }
29466
29657
  removeOrgDomain(id) {
29467
- return this.req("DELETE", `/org/domains/${id}`);
29658
+ return this.req("DELETE", `/org/domains/${encodeURIComponent(id)}`);
29468
29659
  }
29469
29660
  // VT-485/486: enterprise API tokens for SCIM provisioning and SIEM event export. The secret is
29470
29661
  // returned only on create; thereafter only the prefix is shown.
@@ -29475,7 +29666,7 @@ var KutrionClient = class {
29475
29666
  return this.req("POST", "/api-tokens", input);
29476
29667
  }
29477
29668
  revokeApiToken(id) {
29478
- return this.req("DELETE", `/api-tokens/${id}`);
29669
+ return this.req("DELETE", `/api-tokens/${encodeURIComponent(id)}`);
29479
29670
  }
29480
29671
  // #662 agent access plane: non-human identities (#664), their activity (#665) and the kill
29481
29672
  // switch (#675). The credential is returned only on create; thereafter only the prefix is shown.
@@ -29490,23 +29681,29 @@ var KutrionClient = class {
29490
29681
  return this.req("POST", "/agents", input);
29491
29682
  }
29492
29683
  renewAgent(id, ttl_minutes) {
29493
- return this.req("POST", `/agents/${id}/renew`, { ttl_minutes });
29684
+ return this.req("POST", `/agents/${encodeURIComponent(id)}/renew`, { ttl_minutes });
29494
29685
  }
29495
29686
  revokeAgent(id) {
29496
- return this.req("DELETE", `/agents/${id}`);
29687
+ return this.req("DELETE", `/agents/${encodeURIComponent(id)}`);
29497
29688
  }
29498
29689
  // #2700: PUT replaces the whole resource scope; an omitted or empty list means unrestricted.
29499
29690
  agentScopeOptions() {
29500
29691
  return this.req("GET", "/agents/scope-options");
29501
29692
  }
29502
29693
  setAgentResourceScope(id, scope) {
29503
- return this.req("PUT", `/agents/${id}/resource-scope`, scope);
29694
+ return this.req("PUT", `/agents/${encodeURIComponent(id)}/resource-scope`, scope);
29504
29695
  }
29505
29696
  listAgentOidcTrusts() {
29506
29697
  return this.req("GET", "/agents/oidc/trusts");
29507
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
+ }
29508
29705
  setAgentOidcTrustResourceScope(id, scope, opts2 = {}) {
29509
- return this.req("PUT", `/agents/oidc/trusts/${id}/resource-scope`, { ...scope, ...opts2 });
29706
+ return this.req("PUT", `/agents/oidc/trusts/${encodeURIComponent(id)}/resource-scope`, { ...scope, ...opts2 });
29510
29707
  }
29511
29708
  revokeAgents(input) {
29512
29709
  return this.req(
@@ -29523,7 +29720,7 @@ var KutrionClient = class {
29523
29720
  return this.req("GET", qs ? `/agents/sessions?${qs}` : "/agents/sessions");
29524
29721
  }
29525
29722
  getAgentSession(id) {
29526
- return this.req("GET", `/agents/sessions/${id}`);
29723
+ return this.req("GET", `/agents/sessions/${encodeURIComponent(id)}`);
29527
29724
  }
29528
29725
  listAgentActivity(opts2 = {}) {
29529
29726
  const q = new URLSearchParams();
@@ -29533,7 +29730,7 @@ var KutrionClient = class {
29533
29730
  }
29534
29731
  /** Relative path for the auditor export; the caller downloads it with its own auth. */
29535
29732
  agentSessionExportPath(id) {
29536
- return `/agents/sessions/${id}/export`;
29733
+ return `/agents/sessions/${encodeURIComponent(id)}/export`;
29537
29734
  }
29538
29735
  // #669: the human half of just-in-time agent approval. These routes are deliberately unreachable
29539
29736
  // from an MCP client — the agent opens a request and settles nothing, a person in an authenticated
@@ -29544,13 +29741,13 @@ var KutrionClient = class {
29544
29741
  return this.req("GET", "/agent-approvals");
29545
29742
  }
29546
29743
  getAgentApproval(id) {
29547
- return this.req("GET", `/agent-approvals/${id}`);
29744
+ return this.req("GET", `/agent-approvals/${encodeURIComponent(id)}`);
29548
29745
  }
29549
29746
  approveAgentApproval(id, note) {
29550
- return this.req("POST", `/agent-approvals/${id}/approve`, note ? { note } : {});
29747
+ return this.req("POST", `/agent-approvals/${encodeURIComponent(id)}/approve`, note ? { note } : {});
29551
29748
  }
29552
29749
  denyAgentApproval(id, note) {
29553
- return this.req("POST", `/agent-approvals/${id}/deny`, note ? { note } : {});
29750
+ return this.req("POST", `/agent-approvals/${encodeURIComponent(id)}/deny`, note ? { note } : {});
29554
29751
  }
29555
29752
  // #670 (epic #662): the outbound MCP gateway's control plane. Curation is ORG ADMIN; only the
29556
29753
  // report-what-I-run call is open to any member, and that asymmetry is deliberate — the machines
@@ -29573,14 +29770,14 @@ var KutrionClient = class {
29573
29770
  /** Aim a credential at an approved entry. Authorised by WRITE on the credential's own vault, not by
29574
29771
  * org admin — binding is an act of delegation, so 403 here is about the vault and not the console. */
29575
29772
  bindMcpCredential(entryId, credential_id) {
29576
- return this.req("POST", `/mcp/gateway/servers/${entryId}/credential`, { credential_id });
29773
+ return this.req("POST", `/mcp/gateway/servers/${encodeURIComponent(entryId)}/credential`, { credential_id });
29577
29774
  }
29578
29775
  unbindMcpCredential(entryId) {
29579
- return this.req("DELETE", `/mcp/gateway/servers/${entryId}/credential`);
29776
+ return this.req("DELETE", `/mcp/gateway/servers/${encodeURIComponent(entryId)}/credential`);
29580
29777
  }
29581
29778
  /** Stops traffic on the next call, with nothing cached in between. */
29582
29779
  revokeMcpServer(entryId) {
29583
- return this.req("DELETE", `/mcp/gateway/servers/${entryId}`);
29780
+ return this.req("DELETE", `/mcp/gateway/servers/${encodeURIComponent(entryId)}`);
29584
29781
  }
29585
29782
  listMcpObserved() {
29586
29783
  return this.req("GET", "/mcp/gateway/observed");
@@ -29613,10 +29810,10 @@ var KutrionClient = class {
29613
29810
  // which leaves it as it is — a connection could otherwise never be made direct again.
29614
29811
  // #2621: `vault_id` shares a personal host into that vault, one way; its credential must live there.
29615
29812
  updateConnection(connId, input) {
29616
- return this.req("PATCH", `/ssh/connections/${connId}`, input);
29813
+ return this.req("PATCH", `/ssh/connections/${encodeURIComponent(connId)}`, input);
29617
29814
  }
29618
29815
  deleteConnection(connId) {
29619
- return this.req("DELETE", `/ssh/connections/${connId}`);
29816
+ return this.req("DELETE", `/ssh/connections/${encodeURIComponent(connId)}`);
29620
29817
  }
29621
29818
  // #414: self-serve secure-session demo. status drives the empty-state affordance; provision is
29622
29819
  // idempotent (reattaches to an existing demo connection); reset removes the demo artifacts.
@@ -29631,8 +29828,9 @@ var KutrionClient = class {
29631
29828
  }
29632
29829
  // Pre-flight risk guardrail (#141): a high-risk command 403s with { error: "step_up_required" }
29633
29830
  // until the session completes WebAuthn step-up; retry after stepping up.
29634
- exec(connId, command) {
29635
- return this.req("POST", `/ssh/connections/${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 });
29636
29834
  }
29637
29835
  validateCommand(command) {
29638
29836
  return this.req("POST", "/ssh/validate", { command });
@@ -29649,7 +29847,7 @@ var KutrionClient = class {
29649
29847
  *
29650
29848
  * `rdp.tunnel` is deliberately not mintable here — the RDP broker returns its own on the
29651
29849
  * session-open response (`tunnel.ws_ticket`), and this route refuses the scope so a caller without
29652
- * 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).
29653
29851
  */
29654
29852
  mintWsTicket(scope, resourceId) {
29655
29853
  return this.req(
@@ -29715,7 +29913,7 @@ var KutrionClient = class {
29715
29913
  }
29716
29914
  /** 403 step_up_required until the session has a fresh passkey/MFA check; retry after stepping up. */
29717
29915
  clearSshHostKey(id) {
29718
- return this.req("DELETE", `/ssh/host-keys/${id}`);
29916
+ return this.req("DELETE", `/ssh/host-keys/${encodeURIComponent(id)}`);
29719
29917
  }
29720
29918
  /**
29721
29919
  * #2743: pins the key a host_key_unconfirmed refusal showed this caller. For a first-use refusal the server accepts only the
@@ -29733,7 +29931,7 @@ var KutrionClient = class {
29733
29931
  }
29734
29932
  /** 404 when the session already ended, or is not in the caller's org. */
29735
29933
  endLiveSession(kind, id) {
29736
- return this.req("DELETE", `/org/live-sessions/${kind}/${encodeURIComponent(id)}`);
29934
+ return this.req("DELETE", `/org/live-sessions/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`);
29737
29935
  }
29738
29936
  // --- SSH certificate authority (VT-484): mint short-lived user certs from a per-org internal CA ---
29739
29937
  getSshCa() {
@@ -29828,29 +30026,29 @@ var KutrionClient = class {
29828
30026
  // It does NOT mean "the whole directory": the routes default to limit=1000 and report `hasMore`.
29829
30027
  // --- SFTP file transfer (VT-112) ---
29830
30028
  sftpList(connId, path = ".", page) {
29831
- return this.req("GET", `/ssh/connections/${connId}/sftp/list?path=${encodeURIComponent(path)}${listPageQuery(page)}`);
30029
+ return this.req("GET", `/ssh/connections/${encodeURIComponent(connId)}/sftp/list?path=${encodeURIComponent(path)}${listPageQuery(page)}`);
29832
30030
  }
29833
30031
  sftpMkdir(connId, path) {
29834
- return this.req("POST", `/ssh/connections/${connId}/sftp/mkdir`, { path });
30032
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/mkdir`, { path });
29835
30033
  }
29836
30034
  sftpRename(connId, from, to) {
29837
- return this.req("POST", `/ssh/connections/${connId}/sftp/rename`, { from, to });
30035
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/rename`, { from, to });
29838
30036
  }
29839
30037
  sftpDelete(connId, path, isDir) {
29840
- return this.req("POST", `/ssh/connections/${connId}/sftp/delete`, { path, is_dir: isDir });
30038
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/delete`, { path, is_dir: isDir });
29841
30039
  }
29842
30040
  // #587: SFTP checksums + duplicate detection (mirror the file-connection routes).
29843
30041
  sftpChecksum(connId, path, algos = ["sha256"]) {
29844
- return this.req("GET", `/ssh/connections/${connId}/sftp/checksum?path=${encodeURIComponent(path)}&algos=${algos.join(",")}`);
30042
+ return this.req("GET", `/ssh/connections/${encodeURIComponent(connId)}/sftp/checksum?path=${encodeURIComponent(path)}&algos=${algos.join(",")}`);
29845
30043
  }
29846
30044
  sftpDuplicates(connId, path = ".", opts2) {
29847
- return this.req("POST", `/ssh/connections/${connId}/sftp/duplicates`, { path, maxFiles: opts2?.maxFiles, maxBytesPerFile: opts2?.maxBytesPerFile });
30045
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/duplicates`, { path, maxFiles: opts2?.maxFiles, maxBytesPerFile: opts2?.maxBytesPerFile });
29848
30046
  }
29849
30047
  // Download a remote file as a Blob (carries the auth token). With { redact: true } the server
29850
30048
  // routes the bytes through the #142 redaction gate (text only; binary → 415) before sending (#320c).
29851
30049
  async sftpDownload(connId, path, opts2) {
29852
30050
  const tok = this.getToken();
29853
- const res = await this.f(`${this.baseUrl}/ssh/connections/${connId}/sftp/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, {
30051
+ const res = await this.f(`${this.baseUrl}/ssh/connections/${encodeURIComponent(connId)}/sftp/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, {
29854
30052
  headers: this.rawHeaders(tok)
29855
30053
  });
29856
30054
  if (!res.ok) {
@@ -29906,12 +30104,12 @@ var KutrionClient = class {
29906
30104
  // Stream a remote file's bytes instead of buffering them into a Blob. `opts.start` resumes from an
29907
30105
  // offset via Range (#555); read `resumedAt` off the result rather than assuming the server obeyed.
29908
30106
  sftpDownloadStream(connId, path, opts2) {
29909
- return this.downloadStream(`${this.baseUrl}/ssh/connections/${connId}/sftp/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, opts2);
30107
+ return this.downloadStream(`${this.baseUrl}/ssh/connections/${encodeURIComponent(connId)}/sftp/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, opts2);
29910
30108
  }
29911
30109
  // Upload from a byte source without materialising it. `streamed` on the result reports whether this
29912
30110
  // runtime actually streamed the request body or had to collect it first.
29913
30111
  sftpUploadStream(connId, path, body, opts2) {
29914
- return this.uploadStream(`${this.baseUrl}/ssh/connections/${connId}/sftp/upload?path=${encodeURIComponent(path)}`, body, opts2);
30112
+ return this.uploadStream(`${this.baseUrl}/ssh/connections/${encodeURIComponent(connId)}/sftp/upload?path=${encodeURIComponent(path)}`, body, opts2);
29915
30113
  }
29916
30114
  // #2556: throws UploadTooLargeToInspectError when this org inspects uploads and `size` is over what the
29917
30115
  // inspection can hold, so the file is refused before any of it is sent. A policy that cannot be read
@@ -29926,7 +30124,7 @@ var KutrionClient = class {
29926
30124
  throw new UploadTooLargeToInspectError(size, insp.max_bytes, insp.enabled ? "too_large_for_scan" : "too_large_for_policy");
29927
30125
  }
29928
30126
  sftpUploadResumable(connId, path, source, opts2) {
29929
- return this.uploadResumable(`/ssh/connections/${connId}/sftp/upload`, path, source, opts2);
30127
+ return this.uploadResumable(`/ssh/connections/${encodeURIComponent(connId)}/sftp/upload`, path, source, opts2);
29930
30128
  }
29931
30129
  // #2556: one request per UPLOAD_CHUNK_BYTES over the #1566 resume protocol, so a file of any size
29932
30130
  // crosses an edge that caps a request body, and a dropped chunk is re-sent from the offset the
@@ -30051,30 +30249,41 @@ var KutrionClient = class {
30051
30249
  listFileConnections() {
30052
30250
  return this.req("GET", "/files/connections");
30053
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
+ }
30054
30262
  createFileConnection(input) {
30055
30263
  return this.req("POST", "/files/connections", input);
30056
30264
  }
30265
+ // #3456: `username: null` switches the connection to inherit the credential's username.
30057
30266
  updateFileConnection(id, input) {
30058
- return this.req("PATCH", `/files/connections/${id}`, input);
30267
+ return this.req("PATCH", `/files/connections/${encodeURIComponent(id)}`, input);
30059
30268
  }
30060
30269
  deleteFileConnection(id) {
30061
- return this.req("DELETE", `/files/connections/${id}`);
30270
+ return this.req("DELETE", `/files/connections/${encodeURIComponent(id)}`);
30062
30271
  }
30063
30272
  fileList(connId, path = "/", page) {
30064
- return this.req("GET", `/files/connections/${connId}/list?path=${encodeURIComponent(path)}${listPageQuery(page)}`);
30273
+ return this.req("GET", `/files/connections/${encodeURIComponent(connId)}/list?path=${encodeURIComponent(path)}${listPageQuery(page)}`);
30065
30274
  }
30066
30275
  // #587: checksums + duplicate detection.
30067
30276
  fileChecksum(connId, path, algos = ["sha256"]) {
30068
- return this.req("GET", `/files/connections/${connId}/checksum?path=${encodeURIComponent(path)}&algos=${algos.join(",")}`);
30277
+ return this.req("GET", `/files/connections/${encodeURIComponent(connId)}/checksum?path=${encodeURIComponent(path)}&algos=${algos.join(",")}`);
30069
30278
  }
30070
30279
  fileDuplicates(connId, path = "/", opts2) {
30071
- return this.req("POST", `/files/connections/${connId}/duplicates`, { path, maxFiles: opts2?.maxFiles, maxBytesPerFile: opts2?.maxBytesPerFile });
30280
+ return this.req("POST", `/files/connections/${encodeURIComponent(connId)}/duplicates`, { path, maxFiles: opts2?.maxFiles, maxBytesPerFile: opts2?.maxBytesPerFile });
30072
30281
  }
30073
30282
  // #586: recursive CONTENT search under `path`. Bytes are read on the server and only path/line/
30074
30283
  // snippet come back. `glob` filters which files are opened by name; it does not match names into
30075
30284
  // the result. Check `truncated` before presenting the matches as the whole answer.
30076
30285
  fileSearch(connId, path, query, opts2) {
30077
- return this.req("POST", `/files/connections/${connId}/search`, {
30286
+ return this.req("POST", `/files/connections/${encodeURIComponent(connId)}/search`, {
30078
30287
  path,
30079
30288
  query,
30080
30289
  glob: opts2?.glob,
@@ -30084,39 +30293,39 @@ var KutrionClient = class {
30084
30293
  }
30085
30294
  // #588: time-limited presigned direct-download URL (S3 only).
30086
30295
  filePresign(connId, path, expiresSec = 3600) {
30087
- return this.req("GET", `/files/connections/${connId}/presign?path=${encodeURIComponent(path)}&expires=${expiresSec}`);
30296
+ return this.req("GET", `/files/connections/${encodeURIComponent(connId)}/presign?path=${encodeURIComponent(path)}&expires=${expiresSec}`);
30088
30297
  }
30089
30298
  // #574: OAuth cloud connectors (Dropbox/Drive/OneDrive/Box). Tokens stay server-side.
30090
30299
  cloudProviders() {
30091
30300
  return this.req("GET", "/cloud/providers");
30092
30301
  }
30093
30302
  cloudAuthorize(provider, name) {
30094
- return this.req("POST", `/cloud/${provider}/authorize`, { name });
30303
+ return this.req("POST", `/cloud/${encodeURIComponent(provider)}/authorize`, { name });
30095
30304
  }
30096
30305
  listCloudConnections() {
30097
30306
  return this.req("GET", "/cloud/connections");
30098
30307
  }
30099
30308
  deleteCloudConnection(id) {
30100
- return this.req("DELETE", `/cloud/connections/${id}`);
30309
+ return this.req("DELETE", `/cloud/connections/${encodeURIComponent(id)}`);
30101
30310
  }
30102
30311
  cloudList(connId, path = "/", page) {
30103
- return this.req("GET", `/cloud/connections/${connId}/list?path=${encodeURIComponent(path)}${listPageQuery(page)}`);
30312
+ return this.req("GET", `/cloud/connections/${encodeURIComponent(connId)}/list?path=${encodeURIComponent(path)}${listPageQuery(page)}`);
30104
30313
  }
30105
30314
  // #608: list + pin the connection's browse root (Shared Drive / SharePoint site / Box folder).
30106
30315
  cloudRoots(connId) {
30107
- return this.req("GET", `/cloud/connections/${connId}/roots`);
30316
+ return this.req("GET", `/cloud/connections/${encodeURIComponent(connId)}/roots`);
30108
30317
  }
30109
30318
  setCloudRoot(connId, root) {
30110
- return this.req("PUT", `/cloud/connections/${connId}/root`, { root });
30319
+ return this.req("PUT", `/cloud/connections/${encodeURIComponent(connId)}/root`, { root });
30111
30320
  }
30112
30321
  cloudMkdir(connId, path) {
30113
- return this.req("POST", `/cloud/connections/${connId}/mkdir`, { path });
30322
+ return this.req("POST", `/cloud/connections/${encodeURIComponent(connId)}/mkdir`, { path });
30114
30323
  }
30115
30324
  cloudRename(connId, from, to) {
30116
- return this.req("POST", `/cloud/connections/${connId}/rename`, { from, to });
30325
+ return this.req("POST", `/cloud/connections/${encodeURIComponent(connId)}/rename`, { from, to });
30117
30326
  }
30118
30327
  cloudDelete(connId, path, isDir) {
30119
- return this.req("POST", `/cloud/connections/${connId}/delete`, { path, is_dir: isDir });
30328
+ return this.req("POST", `/cloud/connections/${encodeURIComponent(connId)}/delete`, { path, is_dir: isDir });
30120
30329
  }
30121
30330
  // #1627: `redact` is the same opt-in the /files and /ssh download helpers take, and the cloud route
30122
30331
  // has honoured it since #1562 (server/src/modules/cloud.ts:461). It only ever ADDS masking — the
@@ -30124,7 +30333,7 @@ var KutrionClient = class {
30124
30333
  // a client whose users cannot ask for less, not one that is safer.
30125
30334
  async cloudDownload(connId, path, opts2) {
30126
30335
  const tok = this.getToken();
30127
- const res = await this.f(`${this.baseUrl}/cloud/connections/${connId}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, { headers: this.rawHeaders(tok) });
30336
+ const res = await this.f(`${this.baseUrl}/cloud/connections/${encodeURIComponent(connId)}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, { headers: this.rawHeaders(tok) });
30128
30337
  if (!res.ok) {
30129
30338
  const errBody = await res.json().catch(() => void 0);
30130
30339
  throw new ApiError(res.status, apiErrorMessage(res.status, errBody), errBody);
@@ -30134,7 +30343,7 @@ var KutrionClient = class {
30134
30343
  // #2768: one request, so `signal` cancels it but there is no per-piece progress to report.
30135
30344
  async cloudUpload(connId, path, data, opts2) {
30136
30345
  const tok = this.getToken();
30137
- const res = await this.fUpload(`${this.baseUrl}/cloud/connections/${connId}/upload?path=${encodeURIComponent(path)}${opts2?.ifAbsent ? "&if_absent=1" : ""}`, {
30346
+ const res = await this.fUpload(`${this.baseUrl}/cloud/connections/${encodeURIComponent(connId)}/upload?path=${encodeURIComponent(path)}${opts2?.ifAbsent ? "&if_absent=1" : ""}`, {
30138
30347
  method: "POST",
30139
30348
  headers: this.rawHeaders(tok, { "Content-Type": "application/octet-stream" }),
30140
30349
  body: data,
@@ -30155,27 +30364,27 @@ var KutrionClient = class {
30155
30364
  // (cloud.ts:453) — downloadStream only ever sends the open-ended form, so that refusal is
30156
30365
  // unreachable from here.
30157
30366
  cloudDownloadStream(connId, path, opts2) {
30158
- return this.downloadStream(`${this.baseUrl}/cloud/connections/${connId}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, opts2);
30367
+ return this.downloadStream(`${this.baseUrl}/cloud/connections/${encodeURIComponent(connId)}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, opts2);
30159
30368
  }
30160
30369
  // POST /cloud/connections/:id/upload (cloud.ts:537) answers `{ ok }` plus the staging keys and NOT
30161
30370
  // `size` — it returns `uploadProgress()` alone, where the file and sftp routes add `path`/`size`
30162
30371
  // (files.ts:1428, ssh.ts:1070). Typed to what this route actually sends: claiming `size: number`
30163
30372
  // here would hand every caller a number that is undefined at runtime.
30164
30373
  cloudUploadStream(connId, path, body, opts2) {
30165
- return this.uploadStream(`${this.baseUrl}/cloud/connections/${connId}/upload?path=${encodeURIComponent(path)}`, body, opts2);
30374
+ return this.uploadStream(`${this.baseUrl}/cloud/connections/${encodeURIComponent(connId)}/upload?path=${encodeURIComponent(path)}`, body, opts2);
30166
30375
  }
30167
30376
  fileMkdir(connId, path) {
30168
- return this.req("POST", `/files/connections/${connId}/mkdir`, { path });
30377
+ return this.req("POST", `/files/connections/${encodeURIComponent(connId)}/mkdir`, { path });
30169
30378
  }
30170
30379
  fileRename(connId, from, to) {
30171
- return this.req("POST", `/files/connections/${connId}/rename`, { from, to });
30380
+ return this.req("POST", `/files/connections/${encodeURIComponent(connId)}/rename`, { from, to });
30172
30381
  }
30173
30382
  fileDelete(connId, path, isDir) {
30174
- return this.req("POST", `/files/connections/${connId}/delete`, { path, is_dir: isDir });
30383
+ return this.req("POST", `/files/connections/${encodeURIComponent(connId)}/delete`, { path, is_dir: isDir });
30175
30384
  }
30176
30385
  async fileDownload(connId, path, opts2) {
30177
30386
  const tok = this.getToken();
30178
- const res = await this.f(`${this.baseUrl}/files/connections/${connId}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, {
30387
+ const res = await this.f(`${this.baseUrl}/files/connections/${encodeURIComponent(connId)}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, {
30179
30388
  headers: this.rawHeaders(tok)
30180
30389
  });
30181
30390
  if (!res.ok) {
@@ -30186,13 +30395,13 @@ var KutrionClient = class {
30186
30395
  }
30187
30396
  // #1542: streaming + resumable counterparts of fileDownload/fileUpload. See downloadStream above.
30188
30397
  fileDownloadStream(connId, path, opts2) {
30189
- return this.downloadStream(`${this.baseUrl}/files/connections/${connId}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, opts2);
30398
+ return this.downloadStream(`${this.baseUrl}/files/connections/${encodeURIComponent(connId)}/download?path=${encodeURIComponent(path)}${opts2?.redact ? "&redact=1" : ""}`, opts2);
30190
30399
  }
30191
30400
  fileUploadStream(connId, path, body, opts2) {
30192
- return this.uploadStream(`${this.baseUrl}/files/connections/${connId}/upload?path=${encodeURIComponent(path)}`, body, opts2);
30401
+ return this.uploadStream(`${this.baseUrl}/files/connections/${encodeURIComponent(connId)}/upload?path=${encodeURIComponent(path)}`, body, opts2);
30193
30402
  }
30194
30403
  fileUploadResumable(connId, path, source, opts2) {
30195
- return this.uploadResumable(`/files/connections/${connId}/upload`, path, source, opts2);
30404
+ return this.uploadResumable(`/files/connections/${encodeURIComponent(connId)}/upload`, path, source, opts2);
30196
30405
  }
30197
30406
  // #325: download a selection of files/folders as one zip, built server-side (works for every protocol).
30198
30407
  async fileArchive(input) {
@@ -30221,7 +30430,7 @@ var KutrionClient = class {
30221
30430
  return this.req("GET", "/files/shares");
30222
30431
  }
30223
30432
  revokeFileShare(id) {
30224
- return this.req("DELETE", `/files/shares/${id}`);
30433
+ return this.req("DELETE", `/files/shares/${encodeURIComponent(id)}`);
30225
30434
  }
30226
30435
  // #1636: the anonymous side of a Vault Drop — the recipient is outside the org and has no account.
30227
30436
  //
@@ -30263,12 +30472,16 @@ var KutrionClient = class {
30263
30472
  return this.req("GET", "/files/jobs");
30264
30473
  }
30265
30474
  getTransferJob(id) {
30266
- return this.req("GET", `/files/jobs/${id}`);
30475
+ return this.req("GET", `/files/jobs/${encodeURIComponent(id)}`);
30267
30476
  }
30268
30477
  // #583: reprioritise or cancel a job that is still QUEUED. A running or finished job answers 409
30269
30478
  // not_queued — the runner has already leased it and there is nothing left to reorder.
30270
30479
  patchTransferJob(id, input) {
30271
- return this.req("PATCH", `/files/jobs/${id}`, input);
30480
+ return this.req("PATCH", `/files/jobs/${encodeURIComponent(id)}`, input);
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`, {});
30272
30485
  }
30273
30486
  // #563: recurring transfer schedules (interval or daily UTC time).
30274
30487
  createSchedule(input) {
@@ -30278,10 +30491,10 @@ var KutrionClient = class {
30278
30491
  return this.req("GET", "/files/schedules");
30279
30492
  }
30280
30493
  setScheduleEnabled(id, enabled) {
30281
- return this.req("PATCH", `/files/schedules/${id}`, { enabled });
30494
+ return this.req("PATCH", `/files/schedules/${encodeURIComponent(id)}`, { enabled });
30282
30495
  }
30283
30496
  deleteSchedule(id) {
30284
- return this.req("DELETE", `/files/schedules/${id}`);
30497
+ return this.req("DELETE", `/files/schedules/${encodeURIComponent(id)}`);
30285
30498
  }
30286
30499
  // #614: folder watchers — poll a source directory and fire a transfer job on new/changed, size-stable
30287
30500
  // files matching the filters. The transfer runs as a durable job.
@@ -30292,10 +30505,10 @@ var KutrionClient = class {
30292
30505
  return this.req("GET", "/files/folder-watchers");
30293
30506
  }
30294
30507
  setFolderWatcherEnabled(id, enabled) {
30295
- return this.req("PATCH", `/files/folder-watchers/${id}`, { enabled });
30508
+ return this.req("PATCH", `/files/folder-watchers/${encodeURIComponent(id)}`, { enabled });
30296
30509
  }
30297
30510
  deleteFolderWatcher(id) {
30298
- return this.req("DELETE", `/files/folder-watchers/${id}`);
30511
+ return this.req("DELETE", `/files/folder-watchers/${encodeURIComponent(id)}`);
30299
30512
  }
30300
30513
  // #627: workflow templates. A saved edit is a NEW VERSION, not a PATCH — 0150's trigger refuses an
30301
30514
  // UPDATE, because a run names the version it used and reproducibility depends on a stored revision
@@ -30304,19 +30517,19 @@ var KutrionClient = class {
30304
30517
  return this.req("GET", "/files/workflow-templates");
30305
30518
  }
30306
30519
  getWorkflowTemplate(id) {
30307
- return this.req("GET", `/files/workflow-templates/${id}`);
30520
+ return this.req("GET", `/files/workflow-templates/${encodeURIComponent(id)}`);
30308
30521
  }
30309
30522
  createWorkflowTemplate(input) {
30310
30523
  return this.req("POST", "/files/workflow-templates", input);
30311
30524
  }
30312
30525
  addWorkflowTemplateVersion(id, input) {
30313
- return this.req("POST", `/files/workflow-templates/${id}/versions`, input);
30526
+ return this.req("POST", `/files/workflow-templates/${encodeURIComponent(id)}/versions`, input);
30314
30527
  }
30315
30528
  deleteWorkflowTemplate(id) {
30316
- return this.req("DELETE", `/files/workflow-templates/${id}`);
30529
+ return this.req("DELETE", `/files/workflow-templates/${encodeURIComponent(id)}`);
30317
30530
  }
30318
30531
  instantiateWorkflowTemplate(id, input) {
30319
- return this.req("POST", `/files/workflow-templates/${id}/instantiate`, input);
30532
+ return this.req("POST", `/files/workflow-templates/${encodeURIComponent(id)}/instantiate`, input);
30320
30533
  }
30321
30534
  // #559: directory sync / mirror. dry_run returns the plan without writing; omit it to apply.
30322
30535
  // #585: `two_way` and `mirror` are mutually exclusive and the server refuses the pair outright
@@ -30366,13 +30579,13 @@ var KutrionClient = class {
30366
30579
  }
30367
30580
  // #557: SFTP POSIX metadata (chmod / set mtime / create symlink).
30368
30581
  sftpChmod(connId, path, mode) {
30369
- return this.req("POST", `/ssh/connections/${connId}/sftp/chmod`, { path, mode });
30582
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/chmod`, { path, mode });
30370
30583
  }
30371
30584
  sftpChtime(connId, path, mtime) {
30372
- return this.req("POST", `/ssh/connections/${connId}/sftp/chtime`, { path, mtime });
30585
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/chtime`, { path, mtime });
30373
30586
  }
30374
30587
  sftpSymlink(connId, target, link) {
30375
- return this.req("POST", `/ssh/connections/${connId}/sftp/symlink`, { target, link });
30588
+ return this.req("POST", `/ssh/connections/${encodeURIComponent(connId)}/sftp/symlink`, { target, link });
30376
30589
  }
30377
30590
  // #2556: resumable, as sftpUpload.
30378
30591
  async fileUpload(connId, path, data, opts2) {
@@ -30391,7 +30604,7 @@ var KutrionClient = class {
30391
30604
  return this.req("GET", "/ssh/recordings");
30392
30605
  }
30393
30606
  getRecording(id) {
30394
- return this.req("GET", `/ssh/recordings/${id}`);
30607
+ return this.req("GET", `/ssh/recordings/${encodeURIComponent(id)}`);
30395
30608
  }
30396
30609
  /** #2631: what was stored for one of the caller's recorded shell sessions, once it has ended. */
30397
30610
  shellSessionReceipt(sessionId) {
@@ -30406,11 +30619,11 @@ var KutrionClient = class {
30406
30619
  return this.req("DELETE", `/ssh/shell-sessions/${encodeURIComponent(sessionId)}`);
30407
30620
  }
30408
30621
  deleteRecording(id) {
30409
- return this.req("DELETE", `/ssh/recordings/${id}`);
30622
+ return this.req("DELETE", `/ssh/recordings/${encodeURIComponent(id)}`);
30410
30623
  }
30411
30624
  // VT-143: generate (or fetch cached) an AI session summary for a recording. regenerate forces recompute.
30412
30625
  summarizeRecording(id, regenerate = false) {
30413
- return this.req("POST", `/ssh/recordings/${id}/summary${regenerate ? "?regenerate=1" : ""}`);
30626
+ return this.req("POST", `/ssh/recordings/${encodeURIComponent(id)}/summary${regenerate ? "?regenerate=1" : ""}`);
30414
30627
  }
30415
30628
  // --- #2628: org recording review and retention, org owners and admins only ---
30416
30629
  getRecordingPolicy() {
@@ -30477,10 +30690,10 @@ var KutrionClient = class {
30477
30690
  return this.req("GET", `/exposure${s ? `?${s}` : ""}`);
30478
30691
  }
30479
30692
  resolveExposure(id, resolution) {
30480
- return this.req("POST", `/exposure/${id}/resolve`, { resolution });
30693
+ return this.req("POST", `/exposure/${encodeURIComponent(id)}/resolve`, { resolution });
30481
30694
  }
30482
30695
  suppressExposure(id, patterns = []) {
30483
- return this.req("POST", `/exposure/${id}/suppress`, { patterns });
30696
+ return this.req("POST", `/exposure/${encodeURIComponent(id)}/suppress`, { patterns });
30484
30697
  }
30485
30698
  // --- integrations hub: outbound alert + SIEM connectors (VT-310 / VT-485) ---
30486
30699
  integrationCatalog() {
@@ -30493,16 +30706,16 @@ var KutrionClient = class {
30493
30706
  return this.req("POST", "/integrations", input);
30494
30707
  }
30495
30708
  updateIntegration(id, input) {
30496
- return this.req("PATCH", `/integrations/${id}`, input);
30709
+ return this.req("PATCH", `/integrations/${encodeURIComponent(id)}`, input);
30497
30710
  }
30498
30711
  deleteIntegration(id) {
30499
- return this.req("DELETE", `/integrations/${id}`);
30712
+ return this.req("DELETE", `/integrations/${encodeURIComponent(id)}`);
30500
30713
  }
30501
30714
  testIntegration(id) {
30502
- return this.req("POST", `/integrations/${id}/test`);
30715
+ return this.req("POST", `/integrations/${encodeURIComponent(id)}/test`);
30503
30716
  }
30504
30717
  listIntegrationDeliveries(id, limit = 50) {
30505
- return this.req("GET", `/integrations/${id}/deliveries?limit=${limit}`);
30718
+ return this.req("GET", `/integrations/${encodeURIComponent(id)}/deliveries?limit=${limit}`);
30506
30719
  }
30507
30720
  // --- HashiCorp Vault secret sync (VT-306): ENTERPRISE-gated (secret_sync). Auth secret is write-only. ---
30508
30721
  listHcvConnections() {
@@ -30512,28 +30725,28 @@ var KutrionClient = class {
30512
30725
  return this.req("POST", "/hashicorp/connections", input);
30513
30726
  }
30514
30727
  updateHcvConnection(id, input) {
30515
- return this.req("PATCH", `/hashicorp/connections/${id}`, input);
30728
+ return this.req("PATCH", `/hashicorp/connections/${encodeURIComponent(id)}`, input);
30516
30729
  }
30517
30730
  deleteHcvConnection(id) {
30518
- return this.req("DELETE", `/hashicorp/connections/${id}`);
30731
+ return this.req("DELETE", `/hashicorp/connections/${encodeURIComponent(id)}`);
30519
30732
  }
30520
30733
  testHcvConnection(id) {
30521
- return this.req("POST", `/hashicorp/connections/${id}/test`);
30734
+ return this.req("POST", `/hashicorp/connections/${encodeURIComponent(id)}/test`);
30522
30735
  }
30523
30736
  listHcvMappings(connId) {
30524
- return this.req("GET", `/hashicorp/connections/${connId}/mappings`);
30737
+ return this.req("GET", `/hashicorp/connections/${encodeURIComponent(connId)}/mappings`);
30525
30738
  }
30526
30739
  createHcvMapping(connId, input) {
30527
- return this.req("POST", `/hashicorp/connections/${connId}/mappings`, input);
30740
+ return this.req("POST", `/hashicorp/connections/${encodeURIComponent(connId)}/mappings`, input);
30528
30741
  }
30529
30742
  deleteHcvMapping(mappingId) {
30530
- return this.req("DELETE", `/hashicorp/mappings/${mappingId}`);
30743
+ return this.req("DELETE", `/hashicorp/mappings/${encodeURIComponent(mappingId)}`);
30531
30744
  }
30532
30745
  syncHcvMapping(mappingId, resolve2) {
30533
- return this.req("POST", `/hashicorp/mappings/${mappingId}/sync${resolve2 ? `?resolve=${resolve2}` : ""}`);
30746
+ return this.req("POST", `/hashicorp/mappings/${encodeURIComponent(mappingId)}/sync${resolve2 ? `?resolve=${resolve2}` : ""}`);
30534
30747
  }
30535
30748
  syncHcvConnection(connId, resolve2) {
30536
- return this.req("POST", `/hashicorp/connections/${connId}/sync${resolve2 ? `?resolve=${resolve2}` : ""}`);
30749
+ return this.req("POST", `/hashicorp/connections/${encodeURIComponent(connId)}/sync${resolve2 ? `?resolve=${resolve2}` : ""}`);
30537
30750
  }
30538
30751
  // --- rdp broker (VT-490) ---
30539
30752
  listRdpConnections() {
@@ -30543,28 +30756,28 @@ var KutrionClient = class {
30543
30756
  return this.req("POST", "/rdp/connections", input);
30544
30757
  }
30545
30758
  updateRdpConnection(id, input) {
30546
- return this.req("PATCH", `/rdp/connections/${id}`, input);
30759
+ return this.req("PATCH", `/rdp/connections/${encodeURIComponent(id)}`, input);
30547
30760
  }
30548
30761
  deleteRdpConnection(id) {
30549
- return this.req("DELETE", `/rdp/connections/${id}`);
30762
+ return this.req("DELETE", `/rdp/connections/${encodeURIComponent(id)}`);
30550
30763
  }
30551
30764
  testRdpConnection(id) {
30552
- return this.req("POST", `/rdp/connections/${id}/test`);
30765
+ return this.req("POST", `/rdp/connections/${encodeURIComponent(id)}/test`);
30553
30766
  }
30554
30767
  startRdpSession(id) {
30555
- return this.req("POST", `/rdp/connections/${id}/session`);
30768
+ return this.req("POST", `/rdp/connections/${encodeURIComponent(id)}/session`);
30556
30769
  }
30557
30770
  listRdpSessions() {
30558
30771
  return this.req("GET", "/rdp/sessions");
30559
30772
  }
30560
30773
  endRdpSession(sessionId) {
30561
- return this.req("DELETE", `/rdp/sessions/${sessionId}`);
30774
+ return this.req("DELETE", `/rdp/sessions/${encodeURIComponent(sessionId)}`);
30562
30775
  }
30563
30776
  // Stream a brokered session's recording (broker proxies it from the gateway). Binary, so it bypasses
30564
30777
  // the JSON req() helper — same shape as sftpDownload/fileDownload.
30565
30778
  async getRdpRecording(sessionId) {
30566
30779
  const tok = this.getToken();
30567
- const res = await this.f(`${this.baseUrl}/rdp/sessions/${sessionId}/recording`, {
30780
+ const res = await this.f(`${this.baseUrl}/rdp/sessions/${encodeURIComponent(sessionId)}/recording`, {
30568
30781
  headers: this.rawHeaders(tok)
30569
30782
  });
30570
30783
  if (!res.ok) throw new ApiError(res.status, apiErrorMessage(res.status));
@@ -30607,7 +30820,7 @@ var KutrionClient = class {
30607
30820
  }
30608
30821
  // Files AI (#325): build a filename regex from natural language, summarize a listing, or propose a
30609
30822
  // rename plan. All run LAN-first; only file NAMES/metadata are sent (never file content), through the
30610
- // redaction gate. The returned regex is server-validated to compile and be free of ReDoS shapes.
30823
+ // redaction gate. The returned regex is server-validated to compile and to lack the most obvious ReDoS shapes only (isSafeRegex is a heuristic: (a|a)+ passes); clients must run it under a time budget.
30611
30824
  aiFilePattern(input) {
30612
30825
  return this.req("POST", "/ai/files/pattern", input);
30613
30826
  }
@@ -30639,16 +30852,16 @@ var KutrionClient = class {
30639
30852
  return this.req("GET", "/jit/requests");
30640
30853
  }
30641
30854
  getJitRequest(id) {
30642
- return this.req("GET", `/jit/requests/${id}`);
30855
+ return this.req("GET", `/jit/requests/${encodeURIComponent(id)}`);
30643
30856
  }
30644
30857
  approveJitRequest(id, note) {
30645
- return this.req("POST", `/jit/requests/${id}/approve`, { note });
30858
+ return this.req("POST", `/jit/requests/${encodeURIComponent(id)}/approve`, { note });
30646
30859
  }
30647
30860
  denyJitRequest(id, note) {
30648
- return this.req("POST", `/jit/requests/${id}/deny`, { note });
30861
+ return this.req("POST", `/jit/requests/${encodeURIComponent(id)}/deny`, { note });
30649
30862
  }
30650
30863
  revokeJitRequest(id) {
30651
- return this.req("POST", `/jit/requests/${id}/revoke`);
30864
+ return this.req("POST", `/jit/requests/${encodeURIComponent(id)}/revoke`);
30652
30865
  }
30653
30866
  // --- rotation copilot (VT-145) ---
30654
30867
  rotationServices() {
@@ -30672,13 +30885,13 @@ var KutrionClient = class {
30672
30885
  return this.req("POST", "/fleet/plan", input);
30673
30886
  }
30674
30887
  executeFleetRun(id) {
30675
- return this.req("POST", `/fleet/runs/${id}/execute`, { confirm: true });
30888
+ return this.req("POST", `/fleet/runs/${encodeURIComponent(id)}/execute`, { confirm: true });
30676
30889
  }
30677
30890
  fleetRuns() {
30678
30891
  return this.req("GET", "/fleet/runs");
30679
30892
  }
30680
30893
  fleetRun(id) {
30681
- return this.req("GET", `/fleet/runs/${id}`);
30894
+ return this.req("GET", `/fleet/runs/${encodeURIComponent(id)}`);
30682
30895
  }
30683
30896
  // --- anomaly baseline (VT-204) ---
30684
30897
  refreshBaseline() {
@@ -30896,50 +31109,65 @@ function parseDestinationRefusal(body) {
30896
31109
  reason: DESTINATION_REFUSAL_REASONS.includes(b.reason) ? b.reason : null
30897
31110
  };
30898
31111
  }
30899
- var DESTINATION_REFUSAL_TITLE = "Destination not authorized";
30900
- var subject = (r) => r.credential ? `the credential "${r.credential}"` : "this credential";
30901
- function destinationRefusalSummary(r) {
30902
- const where = r.side === "source" ? " as the source of this transfer" : r.side === "dest" ? " as the destination of this transfer" : "";
30903
- return `Kutrion did not connect to ${r.destination}${where} and did not send ${subject(r)}.`;
30904
- }
30905
- function destinationRefusalAllowedLabel(r) {
30906
- if (r.allowedDestinations.length) return "It may be presented to these destinations only:";
30907
- if (r.detail) return r.detail;
30908
- return destinationRefusalFault(r) === "kutrion" ? "Kutrion could not check where this credential may be presented." : "No authorized destinations are recorded for this credential.";
30909
- }
30910
- 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.";
30911
- var DESTINATION_REFUSAL_NOT_A_REMEDY = "Re-entering the credential's value here does not change where it may be presented.";
30912
- var DESTINATION_REFUSAL_FAULT_TITLE = "This destination could not be checked";
30913
- 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.";
30914
- var DESTINATION_REFUSAL_CONNECTION_TITLE = "This destination cannot be authorized as written";
30915
- 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.";
30916
- 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.";
30917
- 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) {
30918
31133
  if (r.reason === "host_not_normalizable" || r.reason === "domain_not_normalizable") {
30919
- return DESTINATION_REFUSAL_CONNECTION_TITLE;
31134
+ return securityCopy("destinationRefusal.title.connection", t);
30920
31135
  }
30921
- return destinationRefusalFault(r) === "kutrion" ? DESTINATION_REFUSAL_FAULT_TITLE : DESTINATION_REFUSAL_TITLE;
31136
+ return securityCopy(destinationRefusalFault(r) === "kutrion" ? "destinationRefusal.title.fault" : "destinationRefusal.title", t);
30922
31137
  }
30923
- function destinationRefusalAction(r) {
30924
- if (r.reason === "host_not_normalizable") return DESTINATION_REFUSAL_HOST_ACTION;
30925
- if (r.reason === "domain_not_normalizable") return DESTINATION_REFUSAL_DOMAIN_ACTION;
30926
- 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);
30927
31143
  }
30928
- function destinationRefusalLines(r, indent = " ") {
31144
+ function destinationRefusalLines(r, indent = " ", t) {
30929
31145
  return [
30930
- destinationRefusalTitle(r),
31146
+ destinationRefusalTitle(r, t),
30931
31147
  "",
30932
- destinationRefusalSummary(r),
31148
+ destinationRefusalSummary(r, t),
30933
31149
  "",
30934
- destinationRefusalAllowedLabel(r),
31150
+ destinationRefusalAllowedLabel(r, t),
30935
31151
  ...r.allowedDestinations.map((d) => `${indent}${d}`),
30936
31152
  "",
30937
- destinationRefusalAction(r),
31153
+ destinationRefusalAction(r, void 0, t),
30938
31154
  // Only true of a refusal that turned on where the credential may go. On a fault of ours the secret
30939
31155
  // was never the subject, and the line reads as one more thing the user got wrong.
30940
- ...destinationRefusalFault(r) === "kutrion" ? [] : [DESTINATION_REFUSAL_NOT_A_REMEDY]
31156
+ ...destinationRefusalFault(r) === "kutrion" ? [] : [securityCopy("destinationRefusal.notARemedy", t)]
30941
31157
  ];
30942
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"];
30943
31171
 
30944
31172
  // ../packages/shared/src/credential-size-refusal.ts
30945
31173
  init_define_KT_HUB_FILES();
@@ -31101,6 +31329,16 @@ function platformFlagRefusalText(code) {
31101
31329
  return code && Object.prototype.hasOwnProperty.call(PLATFORM_FLAG_REFUSAL_COPY, code) ? PLATFORM_FLAG_REFUSAL_COPY[code] : null;
31102
31330
  }
31103
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
+
31104
31342
  // ../packages/shared/src/ssh-refusal.ts
31105
31343
  init_define_KT_HUB_FILES();
31106
31344
  var SSH_REFUSAL_CLASSIFICATION = {
@@ -31222,6 +31460,9 @@ var SSH_REFUSAL_CLASSIFICATION = {
31222
31460
  // Something answered on that port and it was not an SSH server; what is listening has to change
31223
31461
  // first, even though the user is who can change it.
31224
31462
  protocol_error: { family: "failed", retry: "pointless" },
31463
+ // #3419: the login was accepted and the host refused a PTY or a shell. The account is set up that
31464
+ // way on the host, so every attempt earns the same answer.
31465
+ shell_refused: { family: "failed", retry: "pointless" },
31225
31466
  // The rest are genuine retry candidates: a transport fact can be different a second later.
31226
31467
  connection_refused: { family: "failed", retry: "possible" },
31227
31468
  host_not_found: { family: "failed", retry: "possible" },
@@ -31343,12 +31584,12 @@ function parseJumpRefusal(body) {
31343
31584
  const code = str4(b.code) ?? str4(b.error);
31344
31585
  if (!isJumpRefusalCode(code)) return null;
31345
31586
  const nested = b.detail && typeof b.detail === "object" ? b.detail : {};
31346
- const pick2 = (k) => b[k] !== void 0 ? b[k] : nested[k];
31347
- const idx = pick2("hop_index");
31348
- 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");
31349
31590
  return {
31350
31591
  code,
31351
- hopConnectionId: str4(pick2("hop_connection_id")),
31592
+ hopConnectionId: str4(pick3("hop_connection_id")),
31352
31593
  hopIndex: typeof idx === "number" && Number.isFinite(idx) ? idx : null,
31353
31594
  detail: typeof b.detail === "string" ? str4(b.detail) : str4(nested.detail) ?? str4(b.error_description),
31354
31595
  allowedDestinations: Array.isArray(allowed) ? allowed.filter((x) => typeof x === "string" && x.trim() !== "") : null
@@ -31407,12 +31648,12 @@ function parseJumpHostInUse(body) {
31407
31648
  var JUMP_HOST_IN_USE_TITLE = "Other connections are reached through this one";
31408
31649
  function jumpHostInUseSummary(r, deletingName) {
31409
31650
  const name = str4(deletingName);
31410
- const subject2 = name ? `\u201C${name}\u201D` : "this connection";
31651
+ const subject = name ? `\u201C${name}\u201D` : "this connection";
31411
31652
  const n = r.usedBy.length;
31412
31653
  if (!n) {
31413
- 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.`;
31414
31655
  }
31415
- 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.";
31416
31657
  }
31417
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.";
31418
31659
 
@@ -32502,6 +32743,12 @@ function subtle() {
32502
32743
  if (!s || typeof globalThis.crypto.getRandomValues !== "function") throw new VaultKeyError("crypto_unavailable");
32503
32744
  return s;
32504
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
+ }
32505
32752
  function fromB64(b64) {
32506
32753
  const s = atob(b64);
32507
32754
  const out = new Uint8Array(s.length);
@@ -32547,6 +32794,10 @@ async function derivePassphraseKek(passphrase, kdf, onProgress) {
32547
32794
  out.fill(0);
32548
32795
  }
32549
32796
  }
32797
+ function generateVaultKey() {
32798
+ subtle();
32799
+ return random(VAULT_KEY_BYTES);
32800
+ }
32550
32801
  async function unwrapVaultKey(wrap, kek, purpose) {
32551
32802
  let bytes;
32552
32803
  try {
@@ -32582,6 +32833,15 @@ async function unlockVaultKey(record, passphrase, onProgress) {
32582
32833
  }
32583
32834
  var dataKeyAad = (vaultId) => enc.encode(`kutrion-vault-key-v1:data-key:${vaultId.toLowerCase()}`);
32584
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
+ }
32585
32845
  async function unwrapVaultDataKey(wrap, vaultKey, vaultId) {
32586
32846
  let bytes;
32587
32847
  try {
@@ -32605,6 +32865,14 @@ async function unwrapVaultDataKey(wrap, vaultKey, vaultId) {
32605
32865
  }
32606
32866
  }
32607
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
+ }
32608
32876
  async function openItemField(dataKey, iv, blob, vaultId, itemId, purpose) {
32609
32877
  let ivBytes, b;
32610
32878
  try {
@@ -32663,6 +32931,40 @@ async function openPrivateItem(item, sealed, vaultKey) {
32663
32931
  ]);
32664
32932
  return { value: value ?? "", notes, custom_fields: fields ? JSON.parse(fields) : [], passphrase, totp };
32665
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
+ }
32666
32968
 
32667
32969
  // ../packages/shared/src/private-vault-copy.ts
32668
32970
  init_define_KT_HUB_FILES();
@@ -32747,6 +33049,7 @@ var PRIVATE_VAULT_COPY = {
32747
33049
 
32748
33050
  // src/args.ts
32749
33051
  init_define_KT_HUB_FILES();
33052
+ var REPEATABLE = /* @__PURE__ */ new Set(["vault", "connection", "scope-vault", "scope-conn"]);
32750
33053
  function parseForwardSpec(spec) {
32751
33054
  const m = spec.match(/^(\d{1,5}):(.+):(\d{1,5})$/);
32752
33055
  if (!m) return null;
@@ -32758,10 +33061,12 @@ function parseForwardSpec(spec) {
32758
33061
  return { localPort, dstHost, dstPort };
32759
33062
  }
32760
33063
  var GLOBAL_FLAGS = { base: "value", json: "boolean" };
32761
- 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"]);
32762
33065
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32763
33066
  "action",
32764
33067
  "agent-forward",
33068
+ "allow-insecure-lan",
33069
+ "audience",
32765
33070
  "base",
32766
33071
  "browser",
32767
33072
  "client",
@@ -32773,6 +33078,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32773
33078
  "conflict",
32774
33079
  "conn",
32775
33080
  "conns",
33081
+ "connection",
32776
33082
  "cred",
32777
33083
  "current",
32778
33084
  "daily-at",
@@ -32788,7 +33094,9 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32788
33094
  "fingerprint",
32789
33095
  "format",
32790
33096
  "from",
33097
+ "help",
32791
33098
  "host",
33099
+ "issuer",
32792
33100
  "insert",
32793
33101
  "intent",
32794
33102
  "json",
@@ -32825,12 +33133,16 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32825
33133
  "reveal",
32826
33134
  "role",
32827
33135
  "scope",
33136
+ "scope-conn",
33137
+ "scope-vault",
32828
33138
  "scopes",
32829
33139
  "secret",
32830
33140
  "service",
32831
33141
  "session",
32832
33142
  "since",
32833
33143
  "status",
33144
+ "stdin",
33145
+ "subject",
32834
33146
  "target",
32835
33147
  "throttle",
32836
33148
  "timeout",
@@ -32848,19 +33160,28 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
32848
33160
  "verify",
32849
33161
  "views",
32850
33162
  "wait",
33163
+ "write-env",
32851
33164
  "yes"
32852
33165
  ]);
32853
33166
  function unknownFlags(flags) {
32854
33167
  return Object.keys(flags).filter((k) => !KNOWN_FLAGS.has(k));
32855
33168
  }
32856
- function parseArgs(argv) {
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
+ }
33176
+ function parseArgs(argv2) {
32857
33177
  const flags = {};
33178
+ const multi = {};
32858
33179
  let at = 0;
32859
- while (at < argv.length && argv[at].startsWith("--")) {
32860
- const key = argv[at].slice(2);
33180
+ while (at < argv2.length && argv2[at].startsWith("--")) {
33181
+ const key = argv2[at].slice(2);
32861
33182
  const kind = GLOBAL_FLAGS[key];
32862
33183
  if (!kind) break;
32863
- const next = argv[at + 1];
33184
+ const next = argv2[at + 1];
32864
33185
  if (kind === "value" && next !== void 0 && !next.startsWith("--")) {
32865
33186
  flags[key] = next;
32866
33187
  at += 2;
@@ -32869,16 +33190,22 @@ function parseArgs(argv) {
32869
33190
  at += 1;
32870
33191
  }
32871
33192
  }
32872
- const cmd = argv[at];
32873
- const rest = argv.slice(at + 1);
33193
+ const cmd = argv2[at];
33194
+ const rest = argv2.slice(at + 1);
32874
33195
  const positionals = [];
33196
+ if (cmd === "exec") return { cmd, positionals: parseExecArgs(rest, flags), flags, multi };
32875
33197
  for (let i = 0; i < rest.length; i++) {
32876
33198
  const a = rest[i];
33199
+ if (a === "--") {
33200
+ positionals.push(...rest.slice(i + 1));
33201
+ break;
33202
+ }
32877
33203
  if (a.startsWith("--")) {
32878
33204
  const key = a.slice(2);
32879
33205
  const next = rest[i + 1];
32880
33206
  if (!SWITCHES.has(key) && next !== void 0 && !next.startsWith("--")) {
32881
33207
  flags[key] = next;
33208
+ if (REPEATABLE.has(key)) (multi[key] ??= []).push(next);
32882
33209
  i++;
32883
33210
  } else {
32884
33211
  flags[key] = true;
@@ -32887,7 +33214,134 @@ function parseArgs(argv) {
32887
33214
  positionals.push(a);
32888
33215
  }
32889
33216
  }
32890
- 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
+ }
32891
33345
  }
32892
33346
 
32893
33347
  // src/history.ts
@@ -32940,7 +33394,7 @@ function exitForStatus(status) {
32940
33394
  init_define_KT_HUB_FILES();
32941
33395
  var import_child_process2 = require("child_process");
32942
33396
  var import_crypto = require("crypto");
32943
- var import_fs2 = require("fs");
33397
+ var import_fs3 = require("fs");
32944
33398
  var import_os = require("os");
32945
33399
  var import_path2 = require("path");
32946
33400
 
@@ -33038,7 +33492,7 @@ function run(bin, args, store, verb, input) {
33038
33492
  var SECRET_TOOL_DIRS = ["/usr/bin", "/bin", "/usr/local/bin", "/run/current-system/sw/bin"];
33039
33493
  var isExecutable = (p) => {
33040
33494
  try {
33041
- (0, import_fs2.accessSync)(p, import_fs2.constants.X_OK);
33495
+ (0, import_fs3.accessSync)(p, import_fs3.constants.X_OK);
33042
33496
  return true;
33043
33497
  } catch {
33044
33498
  return false;
@@ -33157,7 +33611,7 @@ function why(r, ...secrets) {
33157
33611
  function fileStore(file = FILE) {
33158
33612
  let raw;
33159
33613
  try {
33160
- raw = (0, import_fs2.readFileSync)(file, "utf8");
33614
+ raw = (0, import_fs3.readFileSync)(file, "utf8");
33161
33615
  } catch (e) {
33162
33616
  if (e.code === "ENOENT") return {};
33163
33617
  throw new Error(`cannot read the token file ${file}: ${e.message}`);
@@ -33174,9 +33628,9 @@ function fileStore(file = FILE) {
33174
33628
  var dirLocked = false;
33175
33629
  function ensureFileDir() {
33176
33630
  const dir = (0, import_path2.dirname)(FILE);
33177
- (0, import_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
33631
+ (0, import_fs3.mkdirSync)(dir, { recursive: true, mode: 448 });
33178
33632
  if ((0, import_os.platform)() !== "win32") try {
33179
- (0, import_fs2.chmodSync)(dir, 448);
33633
+ (0, import_fs3.chmodSync)(dir, 448);
33180
33634
  } catch {
33181
33635
  }
33182
33636
  else if (!dirLocked) {
@@ -33205,32 +33659,32 @@ function withLockFile(lock, fn, afterJudge) {
33205
33659
  const deadline = Date.now() + LOCK_WAIT_MS;
33206
33660
  for (; ; ) {
33207
33661
  try {
33208
- const fd = (0, import_fs2.openSync)(lock, "wx", 384);
33662
+ const fd = (0, import_fs3.openSync)(lock, "wx", 384);
33209
33663
  try {
33210
- (0, import_fs2.writeSync)(fd, owner);
33664
+ (0, import_fs3.writeSync)(fd, owner);
33211
33665
  } finally {
33212
- (0, import_fs2.closeSync)(fd);
33666
+ (0, import_fs3.closeSync)(fd);
33213
33667
  }
33214
33668
  break;
33215
33669
  } catch (e) {
33216
33670
  const code = e.code;
33217
33671
  if (code !== "EEXIST" && code !== "EPERM" && code !== "EACCES") throw e;
33218
- 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}`);
33219
33673
  try {
33220
- if (Date.now() - (0, import_fs2.statSync)(lock).mtimeMs > LOCK_STALE_MS) {
33221
- 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");
33222
33676
  afterJudge?.();
33223
33677
  const gone = `${lock}.${process.pid}.${(0, import_crypto.randomBytes)(4).toString("hex")}`;
33224
- (0, import_fs2.renameSync)(lock, gone);
33225
- if ((0, import_fs2.readFileSync)(gone, "utf8") === judged) {
33226
- (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 });
33227
33681
  continue;
33228
33682
  }
33229
33683
  try {
33230
- (0, import_fs2.linkSync)(gone, lock);
33684
+ (0, import_fs3.linkSync)(gone, lock);
33231
33685
  } catch {
33232
33686
  }
33233
- (0, import_fs2.rmSync)(gone, { force: true });
33687
+ (0, import_fs3.rmSync)(gone, { force: true });
33234
33688
  }
33235
33689
  } catch {
33236
33690
  }
@@ -33242,7 +33696,7 @@ function withLockFile(lock, fn, afterJudge) {
33242
33696
  return fn();
33243
33697
  } finally {
33244
33698
  try {
33245
- 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 });
33246
33700
  } catch {
33247
33701
  }
33248
33702
  }
@@ -33251,10 +33705,10 @@ var withFileLock = (fn) => withLockFile(`${FILE}.lock`, fn);
33251
33705
  function sweepStaleTemps(file) {
33252
33706
  const dir = (0, import_path2.dirname)(file), prefix = `${(0, import_path2.basename)(file)}.`;
33253
33707
  try {
33254
- for (const n of (0, import_fs2.readdirSync)(dir)) {
33708
+ for (const n of (0, import_fs3.readdirSync)(dir)) {
33255
33709
  if (!n.startsWith(prefix) || !n.endsWith(".tmp")) continue;
33256
33710
  try {
33257
- 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 });
33258
33712
  } catch {
33259
33713
  }
33260
33714
  }
@@ -33264,16 +33718,16 @@ function sweepStaleTemps(file) {
33264
33718
  function writeFileStore(store) {
33265
33719
  const tmp = `${FILE}.${process.pid}.${(0, import_crypto.randomBytes)(6).toString("hex")}.tmp`;
33266
33720
  try {
33267
- (0, import_fs2.closeSync)((0, import_fs2.openSync)(tmp, "wx", 384));
33721
+ (0, import_fs3.closeSync)((0, import_fs3.openSync)(tmp, "wx", 384));
33268
33722
  if ((0, import_os.platform)() === "win32") lockDownWin(tmp, "file");
33269
33723
  else try {
33270
- (0, import_fs2.chmodSync)(tmp, 384);
33724
+ (0, import_fs3.chmodSync)(tmp, 384);
33271
33725
  } catch {
33272
33726
  }
33273
- (0, import_fs2.writeFileSync)(tmp, JSON.stringify(store));
33727
+ (0, import_fs3.writeFileSync)(tmp, JSON.stringify(store));
33274
33728
  for (let i = 0; ; i++) {
33275
33729
  try {
33276
- (0, import_fs2.renameSync)(tmp, FILE);
33730
+ (0, import_fs3.renameSync)(tmp, FILE);
33277
33731
  break;
33278
33732
  } catch (e) {
33279
33733
  const code = e.code;
@@ -33282,7 +33736,7 @@ function writeFileStore(store) {
33282
33736
  }
33283
33737
  }
33284
33738
  } catch (e) {
33285
- (0, import_fs2.rmSync)(tmp, { force: true });
33739
+ (0, import_fs3.rmSync)(tmp, { force: true });
33286
33740
  throw e;
33287
33741
  }
33288
33742
  }
@@ -33293,7 +33747,7 @@ function updateFileStore(mutate) {
33293
33747
  const store = fileStore();
33294
33748
  if (!mutate(store)) return;
33295
33749
  if (Object.keys(store).length) writeFileStore(store);
33296
- else (0, import_fs2.rmSync)(FILE, { force: true });
33750
+ else (0, import_fs3.rmSync)(FILE, { force: true });
33297
33751
  });
33298
33752
  }
33299
33753
  function dropFileEntry(base) {
@@ -34029,15 +34483,18 @@ async function borrowAndEnrol(deps) {
34029
34483
  if (enrolment === "device_clients_disabled" || enrolment === "satellite_update_required" || enrolment === "hub_not_bound") return { ok: false, reason: enrolment };
34030
34484
  return enrolment ? { ok: true, enrolment, hubPublicKey: borrowed.hubPublicKey } : { ok: false, reason: "enrol_refused" };
34031
34485
  }
34032
- function createBorrowSession(store, client, resolveDeps) {
34486
+ function createBorrowSession(store, client, resolveDeps, otherSigninHeld = () => false) {
34033
34487
  let pending = null;
34034
34488
  let held = null;
34035
34489
  let pin = null;
34036
34490
  let running = false;
34491
+ let autoTried = false;
34037
34492
  return {
34038
- begin() {
34039
- if (running) return { ok: false, reason: "already_enrolled" };
34040
- if (held) return { ok: false, reason: "already_enrolled" };
34493
+ begin(auto = false) {
34494
+ if (auto && autoTried) return { ok: false, reason: "auto_done" };
34495
+ if (auto) autoTried = true;
34496
+ if (running) return { ok: false, reason: "busy" };
34497
+ if (held || otherSigninHeld()) return { ok: false, reason: "signin_in_progress" };
34041
34498
  const p = prepareBorrow(store, client);
34042
34499
  pending = p.ok ? p : null;
34043
34500
  return p.ok ? { ok: true, fingerprint: p.fingerprint } : { ok: false, reason: p.reason };
@@ -34064,6 +34521,9 @@ function createBorrowSession(store, client, resolveDeps) {
34064
34521
  pin = deps.pin;
34065
34522
  return { ok: true, deviceId: out.deviceId, firstContact: out.held !== null };
34066
34523
  },
34524
+ suppressAuto() {
34525
+ autoTried = true;
34526
+ },
34067
34527
  cancel() {
34068
34528
  pending = null;
34069
34529
  },
@@ -34241,14 +34701,29 @@ function joinedAccount(user, lastAccount) {
34241
34701
  accountChanged: lastAccount !== null && lastAccount !== user.id
34242
34702
  };
34243
34703
  }
34704
+ var HELD_TTL_MS = 3e5;
34244
34705
  function createBorrowConfirm(deps) {
34706
+ const ttl = deps.ttlMs ?? HELD_TTL_MS;
34707
+ const now = deps.now ?? Date.now;
34245
34708
  let account = null;
34246
34709
  let heldId = null;
34247
34710
  let heldName = null;
34711
+ let heldAt = 0;
34712
+ let timer = null;
34248
34713
  const remember = (user) => {
34249
34714
  if (deps.lastAccount.get() !== user.id) deps.lastAccount.set(user.id);
34250
34715
  };
34716
+ const expired = () => account !== null && now() - heldAt >= ttl;
34717
+ const reap = () => {
34718
+ if (!expired()) return false;
34719
+ void reject().then(() => deps.onExpired?.());
34720
+ return true;
34721
+ };
34251
34722
  const reject = async () => {
34723
+ if (timer) {
34724
+ clearTimeout(timer);
34725
+ timer = null;
34726
+ }
34252
34727
  account = null;
34253
34728
  heldId = null;
34254
34729
  heldName = null;
@@ -34262,19 +34737,28 @@ function createBorrowConfirm(deps) {
34262
34737
  account = user;
34263
34738
  heldId = deviceId;
34264
34739
  heldName = deviceName ?? null;
34740
+ heldAt = now();
34741
+ if (timer) clearTimeout(timer);
34742
+ timer = setTimeout(reap, ttl);
34743
+ timer.unref?.();
34265
34744
  },
34266
- pending: () => account ? { ...joinedAccount(account, deps.lastAccount.get()), deviceName: heldName } : null,
34745
+ pending: () => account && !reap() ? { ...joinedAccount(account, deps.lastAccount.get()), deviceName: heldName, deviceId: heldId } : null,
34267
34746
  seen(user) {
34268
34747
  if (!account && user) remember(user);
34269
34748
  },
34270
- async confirm() {
34749
+ async confirm(deviceId) {
34750
+ if (reap()) return false;
34271
34751
  const user = account;
34272
34752
  const id = heldId;
34273
- if (!user || !id) return false;
34753
+ if (!user || !id || id !== deviceId) return false;
34274
34754
  if (!deps.borrow.commit(id)) {
34275
34755
  await reject();
34276
34756
  return false;
34277
34757
  }
34758
+ if (timer) {
34759
+ clearTimeout(timer);
34760
+ timer = null;
34761
+ }
34278
34762
  account = null;
34279
34763
  heldId = null;
34280
34764
  remember(user);
@@ -34476,6 +34960,10 @@ function cliHubStores(base, kc, keychainAvailable) {
34476
34960
  };
34477
34961
  return { store, pin, latch, lastAccount, failedWrite: () => failed2 ? { deviceId, bearer } : null };
34478
34962
  }
34963
+ var lastAccountId = (base, kc) => read(kc, slot.lastAccount(base));
34964
+ function recordLastAccountId(base, kc, user) {
34965
+ if (user && typeof user.id === "string" && user.id) write(kc, slot.lastAccount(base), user.id);
34966
+ }
34479
34967
  function borrowedDeviceKey(base, kc) {
34480
34968
  return readDevice(kc, base).key;
34481
34969
  }
@@ -34576,6 +35064,7 @@ async function hubLogin(deps) {
34576
35064
  if (!deps.keychainAvailable()) return { kind: "fallback", note: null, reason: "no_keychain" };
34577
35065
  if (locationFault(deps.location)) return { kind: "fallback", note: null, reason: "hub_not_running" };
34578
35066
  const { store, pin, latch, lastAccount, failedWrite } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
35067
+ if (deps.pairedOnly && !pin.hubPublicKey) return { kind: "fallback", note: null, reason: "not_paired" };
34579
35068
  const revoke = (deviceId, bearer) => endServerSession(deps.call, deviceId, bearer);
34580
35069
  const session = createBorrowSession(store, "cli", () => ({
34581
35070
  location: deps.location,
@@ -34627,11 +35116,36 @@ async function hubLogin(deps) {
34627
35116
  const verdict = await presentJoined(confirm, deps.out, deps.ask);
34628
35117
  if (verdict === "not_kept") return { kind: "fallback", note: HUB_COPY.notKept, reason: "not_kept" };
34629
35118
  if (verdict === "not_you") return { kind: "fallback", note: HUB_COPY.notYou, reason: "not_you" };
34630
- if (!await confirm.confirm()) return { kind: "fallback", note: HUB_COPY.notKept, reason: "not_kept" };
35119
+ if (!await confirm.confirm(done.deviceId)) return { kind: "fallback", note: HUB_COPY.notKept, reason: "not_kept" };
34631
35120
  const lost = await unkept();
34632
35121
  if (lost) return lost;
34633
35122
  return { kind: "signed_in", user, firstContact: true };
34634
35123
  }
35124
+ var canPromptYes = (stdin, stderr) => Boolean(stdin.isTTY && stderr.isTTY);
35125
+ var reborrowFromHub = (deps) => hubLogin({ ...deps, pairedOnly: true });
35126
+ var loginTriesHub = (flags, accountRoot) => flags.email === void 0 && flags["password-stdin"] !== true && accountRoot !== null;
35127
+ var HubAccountSwitchedError = class extends Error {
35128
+ constructor(email) {
35129
+ super(`Signed in through Kutrion Hub as ${email === null ? "an account it could not name" : printable(email)}, which is not the account this command started on. Run it again to use that account.`);
35130
+ this.name = "HubAccountSwitchedError";
35131
+ }
35132
+ };
35133
+ async function retryAfterHubReborrow(run2, deps) {
35134
+ try {
35135
+ return await run2();
35136
+ } catch (e) {
35137
+ if (!deps.refusedSession(e) || deps.wrote()) throw e;
35138
+ const before = deps.lastAccount();
35139
+ const hub = await deps.reborrow();
35140
+ if (hub.kind !== "signed_in") {
35141
+ if (hub.note) deps.out(hub.note);
35142
+ throw e;
35143
+ }
35144
+ if (!before || !hub.user || hub.user.id !== before) throw new HubAccountSwitchedError(hub.user ? hub.user.email : null);
35145
+ deps.out(`Signed in again with the Kutrion app as ${printable(hub.user.email)}; running the command again.`);
35146
+ }
35147
+ return run2();
35148
+ }
34635
35149
 
34636
35150
  // src/hub/hub-seed.ts
34637
35151
  init_define_KT_HUB_FILES();
@@ -34855,9 +35369,9 @@ function installLocationFromRegistry(regOutput) {
34855
35369
  }
34856
35370
  function underInstallRoot(dir, roots) {
34857
35371
  if (!import_node_path3.win32.isAbsolute(dir)) return false;
34858
- const norm = (p) => import_node_path3.win32.resolve(p).toLowerCase().replace(/[\\/]+$/, "");
34859
- const d = norm(dir);
34860
- 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) + "\\"));
34861
35375
  }
34862
35376
  function hubLauncher(env = {}) {
34863
35377
  const platform3 = env.platform ?? process.platform;
@@ -35009,13 +35523,14 @@ var host = () => (0, import_os3.hostname)().trim().replace(/\.local$/, "") || "t
35009
35523
  var ownDeviceName = () => `Kutrion CLI on ${host()}`.slice(0, 255);
35010
35524
  async function registerOwnDevice(deps) {
35011
35525
  const tty = deps.tty ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
35012
- 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 };
35013
35528
  if (!deps.bearer || !deps.keychainAvailable()) return { kind: "skipped", note: null };
35014
35529
  const { store, failedWrite } = cliHubStores(deps.base, deps.kc, deps.keychainAvailable);
35015
35530
  const key = resolveDeviceKey(store);
35016
35531
  const res = await deps.post(
35017
35532
  "/auth/devices/register",
35018
- { 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 } : {} },
35019
35534
  deps.bearer
35020
35535
  );
35021
35536
  if (!res.ok) {
@@ -35185,16 +35700,16 @@ function redactProxyUrls(text2) {
35185
35700
  function bypassesProxy(host2, port, no) {
35186
35701
  if (!no) return false;
35187
35702
  for (const raw of no.split(/[\s,]+/)) {
35188
- let entry = raw.trim().toLowerCase();
35189
- if (!entry) continue;
35190
- if (entry === "*") return true;
35191
- 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+)$/);
35192
35707
  if (colon) {
35193
35708
  if (colon[2] !== port) continue;
35194
- entry = colon[1];
35709
+ entry2 = colon[1];
35195
35710
  }
35196
- entry = entry.replace(/^\*?\./, "");
35197
- if (host2 === entry || host2.endsWith(`.${entry}`)) return true;
35711
+ entry2 = entry2.replace(/^\*?\./, "");
35712
+ if (host2 === entry2 || host2.endsWith(`.${entry2}`)) return true;
35198
35713
  }
35199
35714
  return false;
35200
35715
  }
@@ -35348,8 +35863,8 @@ function firstBindingWarning(name, group, host2, port) {
35348
35863
  ...group.protocols.length > 1 ? ["", "Authorizing SSH also authorizes SFTP. They are one destination."] : []
35349
35864
  ];
35350
35865
  }
35351
- function removalWarning(name, group, entry, remainingAfter, sameGroupRemaining) {
35352
- 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."] : [];
35353
35868
  if (remainingAfter === 0) {
35354
35869
  return [
35355
35870
  `This is the only destination authorized on "${name}".`,
@@ -35368,7 +35883,7 @@ function removalWarning(name, group, entry, remainingAfter, sameGroupRemaining)
35368
35883
  ];
35369
35884
  }
35370
35885
  return [
35371
- `"${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 ")}.`,
35372
35887
  "",
35373
35888
  `Its other ${group.label} destinations are unchanged.`,
35374
35889
  ...pairNote
@@ -35517,6 +36032,7 @@ async function confirmChainedHostKey(deps, connId, k, own) {
35517
36032
  }
35518
36033
 
35519
36034
  // src/refusal.ts
36035
+ var refreshWhy = (code, noun = "session") => refreshRefusalCopy(code, noun)?.why ?? code;
35520
36036
  var REFUSALS = {
35521
36037
  // #1538 authoring time. Safe to key here even though `jump_cycle` and `jump_too_deep` are also
35522
36038
  // WebSocket codes: this table is read only by refusalLines() on the HTTP path, and wsRefusalLines()
@@ -35538,16 +36054,22 @@ var REFUSALS = {
35538
36054
  why: "A jump host must be one of your own SSH connections, and no connection of yours has that id.",
35539
36055
  action: "Run `kt conns` and pass an id from the first column to --jump."
35540
36056
  },
35541
- // auth-guard.ts:109 (social-session gate) and :167 (adaptive), plus rotation.ts:159, jit.ts:226,
35542
- // fleet.ts:190, agents.ts:146, modules/ssh.ts:727 and :1222. The remedy is mechanical and worth
35543
- // stating exactly, because it is not obvious that it can be satisfied anywhere but here: step-up
35544
- // freshness is a per-USER Redis proof with a ~5 minute TTL (server/src/lib/step-up.ts:16), not a
35545
- // claim baked into this CLI's token. Stepping up in the web app on the same account therefore
35546
- // 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).
35547
36069
  step_up_required: {
35548
36070
  title: "Step-up verification required",
35549
36071
  why: "This action needs a recent passkey or app-MFA check, and this session does not have one.",
35550
- 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."
35551
36073
  },
35552
36074
  // #2225 AC5, and the one shape this table exists for: the server owns the REASON, this client adds
35553
36075
  // the ROUTE. The server's hint names what ended the session (server/src/index.ts for a credential
@@ -35568,6 +36090,49 @@ var REFUSALS = {
35568
36090
  why: "The token stored for this server is no longer accepted, so every command will be refused until this machine signs in again.",
35569
36091
  action: "Run `kt login` to sign in again."
35570
36092
  },
36093
+ // #3444/#3445: POST /auth/refresh terminal verdicts (server/src/modules/devices.ts). Each ends the
36094
+ // stored session, like device_revoked, so the only way forward is a fresh `kt login`. The Hub pair
36095
+ // reuses the extension's sentences word for word.
36096
+ refresh_credentials_changed: {
36097
+ title: "Your password changed",
36098
+ why: refreshWhy("refresh_credentials_changed"),
36099
+ action: "Run `kt login` with your new password."
36100
+ },
36101
+ refresh_hub_signed_out: {
36102
+ title: "This session was signed out",
36103
+ why: refreshWhy("refresh_hub_signed_out"),
36104
+ action: "Run `kt login` to sign in again."
36105
+ },
36106
+ refresh_hub_revoked: {
36107
+ title: "This session was signed out",
36108
+ why: refreshWhy("refresh_hub_revoked", "app"),
36109
+ action: "Run `kt login` to sign in again."
36110
+ },
36111
+ device_revoked: {
36112
+ title: "This device was signed out",
36113
+ why: refreshWhy("device_revoked", "device"),
36114
+ action: "Run `kt login` to sign in again and register it anew."
36115
+ },
36116
+ account_inactive: {
36117
+ title: "This account has ended",
36118
+ why: refreshWhy("account_inactive"),
36119
+ action: "Your administrator can restore the account. Signing in again will not work until they do."
36120
+ },
36121
+ account_expired: {
36122
+ title: "This account has expired",
36123
+ why: refreshWhy("account_expired"),
36124
+ action: "Your administrator can extend the account. Signing in again will not work until they do."
36125
+ },
36126
+ user_inactive: {
36127
+ title: "This account has ended",
36128
+ why: refreshWhy("user_inactive"),
36129
+ action: "Your administrator can restore the account. Signing in again will not work until they do."
36130
+ },
36131
+ org_mismatch: {
36132
+ title: "This session was signed out",
36133
+ why: refreshWhy("org_mismatch"),
36134
+ action: "Run `kt login` to sign in again with your current organization."
36135
+ },
35571
36136
  // auth-guard.ts:164. The distinguishing fact is that this one CANNOT be stepped up out of — it is
35572
36137
  // sent precisely when the account holds no second factor to step up with.
35573
36138
  access_blocked: {
@@ -35649,7 +36214,7 @@ var REFUSALS = {
35649
36214
  why: "Promoting now would leave those hosts refusing every session, because they do not yet accept a certificate from the next CA.",
35650
36215
  action: "Install or verify each host listed by `kt broker-ca status`, or revoke its enrollment, then promote again."
35651
36216
  },
35652
- // 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.
35653
36218
  vault_frozen: {
35654
36219
  title: "Vault is read-only",
35655
36220
  why: "This vault is frozen for writes because the plan's vault limit was exceeded after a downgrade.",
@@ -35675,7 +36240,7 @@ var REFUSALS = {
35675
36240
  why: "This organization keeps AI on its own network, so only agents declared `lan` may act for it.",
35676
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".'
35677
36242
  },
35678
- // files.ts:196.
36243
+ // server/src/modules/files.ts:353.
35679
36244
  tls_downgrade_refused: {
35680
36245
  title: "Transport security cannot be weakened",
35681
36246
  why: "The change would turn TLS off, or allow an unverified certificate, on a connection that already has it on.",
@@ -35731,7 +36296,7 @@ var REFUSALS = {
35731
36296
  why: "The code was valid for a limited time and that time has passed.",
35732
36297
  action: "Run `kt login` again; a new code is emailed when the password is accepted."
35733
36298
  },
35734
- // 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
35735
36300
  // `inject_only`; the name the product uses is "No-reveal", and the second half is stated positively
35736
36301
  // so the credential does not read as broken.
35737
36302
  inject_only: {
@@ -35739,12 +36304,42 @@ var REFUSALS = {
35739
36304
  why: "Kutrion does not display, copy, export or share this credential's value. It is still used to authenticate connections and integrations.",
35740
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."
35741
36306
  },
35742
- // modules/ssh.ts:470, modules/rdp.ts:151.
36307
+ // modules/ssh.ts:478, modules/rdp.ts:151.
35743
36308
  inject_only_requires_write: {
35744
36309
  title: "Write access required for a No-reveal credential",
35745
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.",
35746
36311
  action: "Ask an owner or editor of that vault to make the change, or to grant you write access to it."
35747
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
+ },
35748
36343
  // access-policy.ts:264, reached from a root preHandler over the file family; reason values from AccessDecision.reason. The server sends
35749
36344
  // a bare code here and no hint, so translating `reason` is the whole value this entry adds.
35750
36345
  blocked_by_access_policy: {
@@ -35752,7 +36347,7 @@ var REFUSALS = {
35752
36347
  why: "This organization restricts where its file connections may be reached from, and this request came from outside that.",
35753
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."
35754
36349
  },
35755
- // 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.
35756
36351
  blocked_by_dlp_policy: {
35757
36352
  title: "Blocked by a data-loss-prevention rule",
35758
36353
  why: "The content or the path matched a rule this organization applies to file transfers.",
@@ -35922,8 +36517,14 @@ function contextLines(body, indent) {
35922
36517
  const text2 = Array.isArray(v) ? v.filter((x) => x !== null && typeof x !== "object").map((x) => String(x)).join(", ") : String(v);
35923
36518
  if (text2.trim()) out.push(`${indent}${f}: ${text2}`);
35924
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
+ }
35925
36525
  return out;
35926
36526
  }
36527
+ var scrubInput = (m) => m.replace(/,?\s*(?:received|got)\s+(?:'[^']*'|"[^"]*"|\S+)/gi, "").replace(/\s{2,}/g, " ").replace(/[\s,]+$/, "").trim();
35927
36528
  var STATUS_ACTION = {
35928
36529
  400: "The server could not use the request as sent. Check the command's arguments against `kt --help`.",
35929
36530
  401: "This machine's session was not accepted. Run `kt login` to sign in again.",
@@ -35936,11 +36537,15 @@ var STATUS_ACTION = {
35936
36537
  422: "The server understood the request but refused its values. Check the values passed against `kt --help`.",
35937
36538
  429: "Too many requests in a short period. Wait before retrying rather than looping."
35938
36539
  };
35939
- var SERVER_REFUSALS = /* @__PURE__ */ new Set(["payment_unconfirmed"]);
36540
+ var HUB_SIGN_IN_ACTION = {
36541
+ status: "This machine's session was not accepted. Run `kt login`, it signs in through Kutrion Hub.",
36542
+ signed_out: "Run `kt login`, it signs in through Kutrion Hub."
36543
+ };
36544
+ var SERVER_REFUSALS = /* @__PURE__ */ new Set(["payment_unconfirmed", "tls_certificate_untrusted"]);
35940
36545
  var sentenceKey = (t) => t.toLowerCase().replace(/[^a-z0-9]+/g, "");
35941
36546
  var sameSentence = (a, b) => b !== void 0 && sentenceKey(a) === sentenceKey(b);
35942
36547
  var SIGNIN_CODE_REFUSALS = /* @__PURE__ */ new Set(["email_code_required", "signin_code_cap"]);
35943
- function refusalLines(e, indent = " ") {
36548
+ function refusalLines(e, indent = " ", opts2 = {}) {
35944
36549
  if (!(e instanceof ApiError)) return null;
35945
36550
  if (e.status < 400 || e.status >= 500 && !SERVER_REFUSALS.has(refusalCode(e))) return null;
35946
36551
  const dest = parseDestinationRefusal(e.body);
@@ -35963,11 +36568,16 @@ function refusalLines(e, indent = " ") {
35963
36568
  `${indent}kt conn set <id> --jump none`
35964
36569
  ];
35965
36570
  }
35966
- const code = refusalCode(e);
36571
+ const fromRefresh = e.refreshCode !== void 0 && Object.hasOwn(REFUSALS, e.refreshCode);
36572
+ const code = fromRefresh ? e.refreshCode : refusalCode(e);
35967
36573
  const platformFlag = platformFlagRefusalText(code);
35968
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
+ }
35969
36579
  const copy = Object.hasOwn(REFUSALS, code) ? REFUSALS[code] : void 0;
35970
- const server = SIGNIN_CODE_REFUSALS.has(code) ? null : serverText(e.body);
36580
+ const server = fromRefresh || SIGNIN_CODE_REFUSALS.has(code) ? null : serverText(e.body);
35971
36581
  const context = contextLines(e.body, indent);
35972
36582
  const head = copy ? `${copy.title} (${code}, HTTP ${e.status})` : `Refused by the server (${code}, HTTP ${e.status})`;
35973
36583
  const lines = [head, ""];
@@ -35976,7 +36586,9 @@ function refusalLines(e, indent = " ") {
35976
36586
  if (!copy && !server) lines.push("The server sent no explanation with this refusal.");
35977
36587
  if (context.length) lines.push("", ...context);
35978
36588
  lines.push("");
35979
- if (copy?.action) lines.push(copy.action);
36589
+ const hubAction = !opts2.hubSignIn || e.status !== 401 ? null : code === "session_signed_out" ? HUB_SIGN_IN_ACTION.signed_out : copy ? null : HUB_SIGN_IN_ACTION.status;
36590
+ if (hubAction) lines.push(hubAction);
36591
+ else if (copy?.action) lines.push(copy.action);
35980
36592
  else if (copy) lines.push("Nothing at this terminal changes this answer.");
35981
36593
  else lines.push(STATUS_ACTION[e.status] ?? `Quote the code and status above when reporting this; they are the whole of what the server sent.`);
35982
36594
  return lines;
@@ -36038,7 +36650,9 @@ function wsRefusalLines(frame, indent = " ", jumpCtx = {}) {
36038
36650
  const jump = jumpRefusalReport(frame, code, head, jumpCtx, indent);
36039
36651
  if (jump) return jump;
36040
36652
  const lines = [code && !ourFault ? `${head} (${code})` : head, ""];
36041
- 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
+ );
36042
36656
  if (host2) lines.push("", `${indent}endpoint: ${port === null ? host2 : `${host2}:${port}`}`);
36043
36657
  if (allowed.length) {
36044
36658
  lines.push("", destinationRefusalAllowedLabel(dest));
@@ -36051,6 +36665,17 @@ function wsRefusalLines(frame, indent = " ", jumpCtx = {}) {
36051
36665
  }
36052
36666
 
36053
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
+ }
36054
36679
  function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onChainedHostKey) {
36055
36680
  const wsUrl = shellUrl(base, connId);
36056
36681
  const ws = new wrapper_default(wsUrl, { headers: { Authorization: `Bearer ${token}` }, agent: wsAgentFor(wsUrl) });
@@ -36059,12 +36684,12 @@ function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onCha
36059
36684
  const isTty = Boolean(stdin.isTTY && stdout.isTTY);
36060
36685
  const sendResize = () => {
36061
36686
  if (ws.readyState !== wrapper_default.OPEN) return;
36062
- const cols = stdout.columns ?? 80;
36063
- const rows = stdout.rows ?? 24;
36687
+ const [cols, rows] = ttySize(stdout.columns, stdout.rows);
36064
36688
  ws.send(resizeFrame(cols, rows));
36065
36689
  };
36066
36690
  let exiting = false;
36067
36691
  let recordedSessionId = null;
36692
+ let sessionInfo = null;
36068
36693
  const printReceipt = async () => {
36069
36694
  const id = recordedSessionId;
36070
36695
  if (!id) return;
@@ -36086,6 +36711,9 @@ function runShell(base, token, connId, onExit, jumpCtx = {}, afterSession, onCha
36086
36711
  ws.close();
36087
36712
  } catch {
36088
36713
  }
36714
+ if (!ended && sessionInfo) process.stderr.write(`\r
36715
+ ${detachNotice(sessionInfo)}\r
36716
+ `);
36089
36717
  void (ended ? printReceipt() : Promise.resolve()).catch(() => {
36090
36718
  }).then(() => onExit?.()).catch(() => {
36091
36719
  }).then(() => process.exit(code));
@@ -36126,6 +36754,7 @@ ${wsRefusalLines(frame, " ", jumpCtx).join("\r\n")}\r
36126
36754
  const sessionFrame = sawSession ? null : parseSessionFrame(text2);
36127
36755
  if (sessionFrame) {
36128
36756
  sawSession = true;
36757
+ sessionInfo = sessionFrame;
36129
36758
  const state = recordingStateLabel(sessionFrame);
36130
36759
  if (state) process.stderr.write(`\r
36131
36760
  [Kutrion] ${state}\r
@@ -36203,7 +36832,7 @@ var import_readline = require("readline");
36203
36832
 
36204
36833
  // src/mcp-install.ts
36205
36834
  init_define_KT_HUB_FILES();
36206
- var import_fs3 = require("fs");
36835
+ var import_fs4 = require("fs");
36207
36836
  var import_os4 = require("os");
36208
36837
  var import_path4 = require("path");
36209
36838
  var import_child_process3 = require("child_process");
@@ -36233,6 +36862,18 @@ function refuseBodyRedirects(inner = fetch) {
36233
36862
  }
36234
36863
  };
36235
36864
  }
36865
+ function trackWrites(inner) {
36866
+ let wrote = false;
36867
+ return {
36868
+ fetch: async (input, init) => {
36869
+ const res = await inner(input, init);
36870
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
36871
+ if (res.ok && method !== "GET") wrote = true;
36872
+ return res;
36873
+ },
36874
+ wrote: () => wrote
36875
+ };
36876
+ }
36236
36877
 
36237
36878
  // src/mcp-install.ts
36238
36879
  var appData = () => process.env.APPDATA ?? (0, import_path4.join)((0, import_os4.homedir)(), "AppData", "Roaming");
@@ -36286,35 +36927,35 @@ function bundleCandidates(moduleDir) {
36286
36927
  function resolveLaunch(base, client, moduleDir = __dirname) {
36287
36928
  const which = (0, import_child_process3.spawnSync)("command -v kt", { encoding: "utf8", shell: true });
36288
36929
  const onPath = which.status === 0 ? (which.stdout ?? "").trim().split("\n")[0] : "";
36289
- let entry;
36930
+ let entry2;
36290
36931
  if ((0, import_path4.isAbsolute)(onPath)) {
36291
- entry = { command: onPath, args: ["mcp"] };
36932
+ entry2 = { command: onPath, args: ["mcp"] };
36292
36933
  } else {
36293
- const bundle = bundleCandidates(moduleDir).find((p) => (0, import_fs3.existsSync)(p));
36934
+ const bundle = bundleCandidates(moduleDir).find((p) => (0, import_fs4.existsSync)(p));
36294
36935
  if (!bundle) {
36295
36936
  throw new Error(
36296
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."
36297
36938
  );
36298
36939
  }
36299
- entry = { command: process.execPath, args: [bundle, "mcp"] };
36940
+ entry2 = { command: process.execPath, args: [bundle, "mcp"] };
36300
36941
  }
36301
36942
  const fileStore2 = (ktEnv("TOKEN_STORE") ?? "").trim().toLowerCase() === "file";
36302
- entry.env = {
36943
+ entry2.env = {
36303
36944
  KT_API_BASE: base,
36304
36945
  ...client ? { KT_MCP_CLIENT: client } : {},
36305
36946
  ...fileStore2 ? { KT_TOKEN_STORE: "file" } : {}
36306
36947
  };
36307
- return entry;
36948
+ return entry2;
36308
36949
  }
36309
36950
  var STORE_ENV = /* @__PURE__ */ new Set(["TOKEN_STORE", "TOKEN_STORE_TIMEOUT_MS"]);
36310
- async function verifyLaunch(entry, timeoutMs = 15e3) {
36951
+ async function verifyLaunch(entry2, timeoutMs = 15e3) {
36311
36952
  const probe = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
36312
36953
  const inherited = Object.fromEntries(
36313
36954
  Object.entries(process.env).filter(([k]) => !/^(KT|VT)_/.test(k) || STORE_ENV.has(k.slice(3)))
36314
36955
  );
36315
- const res = (0, import_child_process3.spawnSync)(entry.command, entry.args, {
36956
+ const res = (0, import_child_process3.spawnSync)(entry2.command, entry2.args, {
36316
36957
  input: probe + "\n",
36317
- env: { ...inherited, ...entry.env ?? {} },
36958
+ env: { ...inherited, ...entry2.env ?? {} },
36318
36959
  cwd: (0, import_os4.tmpdir)(),
36319
36960
  encoding: "utf8",
36320
36961
  timeout: timeoutMs
@@ -36333,10 +36974,10 @@ async function verifyLaunch(entry, timeoutMs = 15e3) {
36333
36974
  return { ok: false, error: "the bridge produced output that is not JSON-RPC" };
36334
36975
  }
36335
36976
  }
36336
- function mergeConfig(existing, key, entry) {
36977
+ function mergeConfig(existing, key, entry2) {
36337
36978
  const cfg = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
36338
36979
  const servers = cfg[key] && typeof cfg[key] === "object" && !Array.isArray(cfg[key]) ? { ...cfg[key] } : {};
36339
- servers[SERVER_NAME] = entry;
36980
+ servers[SERVER_NAME] = entry2;
36340
36981
  cfg[key] = servers;
36341
36982
  return cfg;
36342
36983
  }
@@ -36352,14 +36993,14 @@ function removeFromConfig(existing, key) {
36352
36993
  }
36353
36994
  function readJson(path) {
36354
36995
  try {
36355
- return JSON.parse((0, import_fs3.readFileSync)(path, "utf8"));
36996
+ return JSON.parse((0, import_fs4.readFileSync)(path, "utf8"));
36356
36997
  } catch {
36357
36998
  return void 0;
36358
36999
  }
36359
37000
  }
36360
37001
  function writeJson(path, value) {
36361
- (0, import_fs3.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
36362
- (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");
36363
37004
  }
36364
37005
  var agentAccount = (base, client) => client ? `agent:${base}:${client}` : `agent:${base}`;
36365
37006
  function getAgentToken(base, client) {
@@ -36404,7 +37045,7 @@ async function installClient(o) {
36404
37045
  if (!configPath) {
36405
37046
  throw new Error(`${target.label} is not available on this platform (${(0, import_os4.platform)()}).`);
36406
37047
  }
36407
- const entry = resolveLaunch(o.base, o.client);
37048
+ const entry2 = resolveLaunch(o.base, o.client);
36408
37049
  const mint = () => api2(o.base, "/agents", o.userToken, {
36409
37050
  method: "POST",
36410
37051
  body: JSON.stringify({
@@ -36413,7 +37054,8 @@ async function installClient(o) {
36413
37054
  client: o.client,
36414
37055
  scopes: o.scopes,
36415
37056
  ttl_minutes: o.ttlDays * 24 * 60,
36416
- egress_class: o.egressClass ?? "cloud"
37057
+ egress_class: o.egressClass ?? "cloud",
37058
+ ...o.resourceScope ? { resource_scope: o.resourceScope } : {}
36417
37059
  })
36418
37060
  });
36419
37061
  let superseded = 0;
@@ -36454,16 +37096,17 @@ async function installClient(o) {
36454
37096
  superseded += old.revoked;
36455
37097
  revokeFailed.push(...old.revokeFailed);
36456
37098
  agentListStatus ??= old.agentListStatus;
36457
- writeJson(configPath, mergeConfig(readJson(configPath), target.key, entry));
37099
+ writeJson(configPath, mergeConfig(readJson(configPath), target.key, entry2));
36458
37100
  const who = await api2(o.base, "/mcp/whoami", agent.token);
36459
37101
  return {
37102
+ ...o.resourceScope ? { resourceScope: o.resourceScope } : {},
36460
37103
  configPath,
36461
37104
  principalId: agent.id,
36462
37105
  expiresAt: agent.expires_at,
36463
37106
  scopes: agent.scopes,
36464
37107
  next: target.next,
36465
37108
  verified: who.status === 200 && who.body?.principal_id === agent.id,
36466
- launch: await verifyLaunch(entry),
37109
+ launch: await verifyLaunch(entry2),
36467
37110
  superseded,
36468
37111
  revokeFailed,
36469
37112
  ...agentListStatus !== void 0 ? { agentListStatus } : {}
@@ -36481,7 +37124,7 @@ async function uninstallClient(o) {
36481
37124
  if (o.client) {
36482
37125
  const target = CLIENTS[o.client];
36483
37126
  configPath = target.path(o.dir);
36484
- if (configPath && (0, import_fs3.existsSync)(configPath)) {
37127
+ if (configPath && (0, import_fs4.existsSync)(configPath)) {
36485
37128
  writeJson(configPath, removeFromConfig(readJson(configPath), target.key));
36486
37129
  }
36487
37130
  }
@@ -36503,6 +37146,7 @@ async function agentStatus(base, token) {
36503
37146
  principal_id: who.body?.principal_id,
36504
37147
  expires_at: who.body?.expires_at,
36505
37148
  scopes: who.body?.scopes,
37149
+ resource_scope: who.body?.resource_scope,
36506
37150
  rate_limit: who.body?.rate_limit
36507
37151
  };
36508
37152
  }
@@ -36518,17 +37162,66 @@ async function mcpStatus(base, dir) {
36518
37162
  const p = t.path(dir);
36519
37163
  let installed = false;
36520
37164
  let configuredBase = null;
36521
- if (p && (0, import_fs3.existsSync)(p)) {
37165
+ if (p && (0, import_fs4.existsSync)(p)) {
36522
37166
  const cfg = readJson(p);
36523
- const entry = cfg?.[t.key]?.[SERVER_NAME];
36524
- installed = !!entry;
36525
- 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;
36526
37170
  }
36527
37171
  return { client: id, label: t.label, path: p, installed, configuredBase, agent: await agentStatus(base, getToken(agentAccount(base, id))) };
36528
37172
  }));
36529
37173
  const legacy = await agentStatus(base, getToken(agentAccount(base)));
36530
37174
  return { keychain: backendName(), hasToken: !!legacy || configs.some((c) => c.agent), configs, legacy };
36531
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
+ }
36532
37225
 
36533
37226
  // src/mcp.ts
36534
37227
  var MODERN = "2026-07-28";
@@ -37286,9 +37979,23 @@ function recoveryText(rec, s) {
37286
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."
37287
37980
  ].join("\n");
37288
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
+ }
37289
37994
 
37290
37995
  // src/private-vault.ts
37291
37996
  init_define_KT_HUB_FILES();
37997
+ var import_fs5 = require("fs");
37998
+ var import_path5 = require("path");
37292
37999
  var PASSPHRASE_STDIN_FLAG = "vault-passphrase-stdin";
37293
38000
  var PrivateVaultError = class extends Error {
37294
38001
  };
@@ -37306,6 +38013,9 @@ There is no flag that takes the passphrase itself: a flag lands in shell history
37306
38013
  return io.ask("vault passphrase: ");
37307
38014
  }
37308
38015
  async function unlockForCommand(client, io) {
38016
+ return (await unlockVault(client, io)).key;
38017
+ }
38018
+ async function unlockVault(client, io) {
37309
38019
  const passphrase = await readVaultPassphrase(io);
37310
38020
  if (!passphrase) throw new PrivateVaultError("no vault passphrase was given");
37311
38021
  let record;
@@ -37318,7 +38028,7 @@ async function unlockForCommand(client, io) {
37318
38028
  throw e;
37319
38029
  }
37320
38030
  try {
37321
- return await unlockVaultKey(record, passphrase);
38031
+ return { key: await unlockVaultKey(record, passphrase), enrolledAt: record.created_at };
37322
38032
  } catch (e) {
37323
38033
  if (e instanceof VaultKeyError && e.code === "wrong_secret") throw new PrivateVaultError(PRIVATE_VAULT_COPY.wrongPassphrase);
37324
38034
  throw e;
@@ -37329,12 +38039,115 @@ async function openSealedItem(client, key, vaultId, credId) {
37329
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.");
37330
38040
  try {
37331
38041
  const opened = await openPrivateItem({ id: credential.id, vault_id: credential.vault_id }, sealed, key);
37332
- return { value: opened.value };
38042
+ return { value: opened.value, notes: opened.notes };
37333
38043
  } catch (e) {
37334
38044
  if (e instanceof VaultKeyError) throw new PrivateVaultError("this item could not be opened with your vault key.");
37335
38045
  throw e;
37336
38046
  }
37337
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`.";
37338
38151
 
37339
38152
  // src/enroll.ts
37340
38153
  init_define_KT_HUB_FILES();
@@ -37415,11 +38228,11 @@ function parsePurposes(v) {
37415
38228
  }
37416
38229
  return out.length ? out : { error: `--purposes is required: one or more of ${ENROLL_PURPOSES.join(", ")}` };
37417
38230
  }
37418
- function parseConfirms(argv) {
38231
+ function parseConfirms(argv2) {
37419
38232
  const out = /* @__PURE__ */ new Map();
37420
- for (let i = 0; i < argv.length; i++) {
37421
- if (argv[i] !== "--confirm") continue;
37422
- const v = argv[i + 1];
38233
+ for (let i = 0; i < argv2.length; i++) {
38234
+ if (argv2[i] !== "--confirm") continue;
38235
+ const v = argv2[i + 1];
37423
38236
  if (v === void 0 || v.startsWith("--")) return { error: "--confirm needs <connId|host>=<fingerprint>" };
37424
38237
  i++;
37425
38238
  for (const pair of v.split(",").map((s) => s.trim()).filter(Boolean)) {
@@ -37544,10 +38357,47 @@ function parseHostsUnconfirmed(body) {
37544
38357
  return { count: typeof b.unconfirmedCount === "number" ? b.unconfirmedCount : hosts.length, hosts };
37545
38358
  }
37546
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
+
37547
38397
  // src/transfer.ts
37548
38398
  init_define_KT_HUB_FILES();
37549
- var import_fs4 = require("fs");
37550
- var import_path5 = require("path");
38399
+ var import_fs6 = require("fs");
38400
+ var import_path6 = require("path");
37551
38401
  var import_promises = require("stream/promises");
37552
38402
  var EXIT_OK = 0;
37553
38403
  var EXIT_FAIL = 1;
@@ -37574,7 +38424,7 @@ function isSideError(s) {
37574
38424
  var listOn = (c, k, id, p) => k === "sftp" ? c.sftpList(id, p) : c.fileList(id, p);
37575
38425
  var mkdirOn = (c, k, id, p) => k === "sftp" ? c.sftpMkdir(id, p) : c.fileMkdir(id, p);
37576
38426
  var downloadOn = (c, k, id, p, o) => k === "sftp" ? c.sftpDownloadStream(id, p, o) : c.fileDownloadStream(id, p, o);
37577
- 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);
37578
38428
  var abortOnHostKey = (e) => {
37579
38429
  if (hostKeyUnconfirmedFromError(e)) throw e;
37580
38430
  };
@@ -37620,6 +38470,8 @@ function exitFor(o) {
37620
38470
  return o.ok > 0 ? EXIT_PARTIAL : EXIT_FAIL;
37621
38471
  }
37622
38472
  var msg = (e) => e instanceof Error ? e.message : String(e);
38473
+ async function* emptyChunks() {
38474
+ }
37623
38475
  async function* counted(chunks, p, tally) {
37624
38476
  for await (const c of chunks) {
37625
38477
  tally.bytes += c.byteLength;
@@ -37633,15 +38485,15 @@ async function saveTo(s, local, append, p) {
37633
38485
  await (0, import_promises.pipeline)(counted(s.chunks, p, tally), process.stdout, { end: false });
37634
38486
  return tally.bytes;
37635
38487
  }
37636
- const dir = (0, import_path5.dirname)(local);
37637
- if (dir && dir !== "." && !(0, import_fs4.existsSync)(dir)) (0, import_fs4.mkdirSync)(dir, { recursive: true });
37638
- 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" } : {}));
37639
38491
  return tally.bytes;
37640
38492
  }
37641
38493
  async function getOne(c, kind, id, remote, local, opts2) {
37642
38494
  let start = 0;
37643
- if (opts2.resume && local !== "-" && (0, import_fs4.existsSync)(local)) {
37644
- 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);
37645
38497
  if (st.isFile()) start = st.size;
37646
38498
  }
37647
38499
  let s;
@@ -37664,7 +38516,7 @@ async function getOne(c, kind, id, remote, local, opts2) {
37664
38516
  opts2.note?.(`${remote}: server ignored the resume range, downloading from the start`);
37665
38517
  start = 0;
37666
38518
  }
37667
- 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);
37668
38520
  const bytes = await saveTo(s, local, start > 0, p);
37669
38521
  p.finish();
37670
38522
  return { bytes };
@@ -37716,13 +38568,13 @@ async function getPath(c, kind, id, remote, local, opts2 = {}) {
37716
38568
  return out;
37717
38569
  }
37718
38570
  for await (const item of walkRemote(c, kind, id, remote)) {
37719
- const dest = (0, import_path5.join)(local, item.rel);
38571
+ const dest = (0, import_path6.join)(local, item.rel);
37720
38572
  if (item.link) {
37721
38573
  opts2.note?.(`skipped symlink ${item.path}`);
37722
38574
  continue;
37723
38575
  }
37724
38576
  if (item.dir) {
37725
- (0, import_fs4.mkdirSync)(dest, { recursive: true });
38577
+ (0, import_fs6.mkdirSync)(dest, { recursive: true });
37726
38578
  continue;
37727
38579
  }
37728
38580
  try {
@@ -37738,8 +38590,8 @@ async function getPath(c, kind, id, remote, local, opts2 = {}) {
37738
38590
  return out;
37739
38591
  }
37740
38592
  function* walkLocal(root, rel = "") {
37741
- for (const e of (0, import_fs4.readdirSync)(root, { withFileTypes: true })) {
37742
- 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);
37743
38595
  const r = rel ? `${rel}/${e.name}` : e.name;
37744
38596
  if (e.isSymbolicLink()) continue;
37745
38597
  if (e.isDirectory()) {
@@ -37751,18 +38603,21 @@ function* walkLocal(root, rel = "") {
37751
38603
  }
37752
38604
  }
37753
38605
  async function putOne(c, kind, id, local, remote, opts2) {
37754
- const total = (0, import_fs4.statSync)(local).size;
37755
- await c.preflightUpload?.(total);
37756
- 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);
37757
38608
  const tally = { bytes: 0 };
37758
- 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);
37759
38614
  p.finish();
37760
38615
  if (!r.streamed) opts2.note?.(`${local}: this runtime could not stream the request body, the file was buffered`);
37761
38616
  return r.size ?? tally.bytes;
37762
38617
  }
37763
38618
  async function putPath(c, kind, id, local, remote, opts2 = {}) {
37764
38619
  const out = empty();
37765
- 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;
37766
38621
  if (!st) {
37767
38622
  out.failed++;
37768
38623
  out.errors.push(`${local}: no such file`);
@@ -37795,7 +38650,7 @@ async function putPath(c, kind, id, local, remote, opts2 = {}) {
37795
38650
  }
37796
38651
  continue;
37797
38652
  }
37798
- const parent = (0, import_path5.dirname)(dest);
38653
+ const parent = (0, import_path6.dirname)(dest);
37799
38654
  if (!made.has(parent)) {
37800
38655
  try {
37801
38656
  await mkdirOn(c, kind, id, parent);
@@ -37814,13 +38669,32 @@ async function putPath(c, kind, id, local, remote, opts2 = {}) {
37814
38669
  }
37815
38670
  return out;
37816
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
+ }
37817
38683
  async function waitForJob(get, id, opts2) {
37818
38684
  const interval = opts2.intervalMs ?? 2e3;
37819
38685
  const deadline = Date.now() + opts2.timeoutMs;
37820
38686
  for (; ; ) {
37821
- const { job } = await get(id);
37822
- opts2.onTick?.(job);
37823
- 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
+ }
37824
38698
  if (Date.now() + interval > deadline) return null;
37825
38699
  await new Promise((r) => setTimeout(r, interval));
37826
38700
  }
@@ -37834,11 +38708,12 @@ function parseDailyAt(v) {
37834
38708
  }
37835
38709
 
37836
38710
  // src/index.ts
37837
- 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"));
37838
38713
  function storedBase() {
37839
38714
  let file;
37840
38715
  try {
37841
- file = JSON.parse((0, import_fs5.readFileSync)(CONFIG, "utf8"));
38716
+ file = JSON.parse((0, import_fs7.readFileSync)(CONFIG, "utf8"));
37842
38717
  } catch {
37843
38718
  return "";
37844
38719
  }
@@ -37856,9 +38731,17 @@ function storedBase() {
37856
38731
  }
37857
38732
  return base;
37858
38733
  }
37859
- function saveCfg(base) {
37860
- (0, import_fs5.mkdirSync)((0, import_path6.dirname)(CONFIG), { recursive: true });
37861
- (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);
37862
38745
  }
37863
38746
  function resolveBase(flag) {
37864
38747
  const explicit = typeof flag === "string" ? flag.trim() : "";
@@ -38005,16 +38888,38 @@ async function readStdin() {
38005
38888
  function readSecret(q) {
38006
38889
  return process.stdin.isTTY ? prompt(q, true) : readStdin();
38007
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.";
38008
38892
  async function openPrivateItem2(client, fromStdin, vaultId, credId) {
38009
38893
  try {
38010
38894
  const key = await unlockForCommand(client, { tty: !!process.stdin.isTTY, fromStdin, ask: promptSecret, readStdin });
38011
- 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}`);
38012
38898
  return null;
38013
38899
  } catch (e) {
38014
38900
  if (e instanceof PrivateVaultError) return e.message;
38015
38901
  throw e;
38016
38902
  }
38017
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 };
38018
38923
  function refuseSecretFlag(flag, instead) {
38019
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.`);
38020
38925
  console.error(` ${instead}`);
@@ -38089,12 +38994,12 @@ async function credDest(client, args, flags, asJson, die) {
38089
38994
  const secret = await promptSecret(`current value for "${name}": `);
38090
38995
  let added;
38091
38996
  try {
38092
- added = await client.addCredentialDestination(vaultId, cid, {
38997
+ added = await withStepUp(client, () => client.addCredentialDestination(vaultId, cid, {
38093
38998
  protocol: group.key,
38094
38999
  host: host2,
38095
39000
  ...portGiven ? { port } : {},
38096
39001
  secret
38097
- });
39002
+ }));
38098
39003
  } catch (e) {
38099
39004
  const text2 = destRefusalText(e);
38100
39005
  if (text2) return die(text2);
@@ -38117,24 +39022,24 @@ async function credDest(client, args, flags, asJson, die) {
38117
39022
  const group = groupFor(row.protocol);
38118
39023
  if (!group) return die(`this destination is recorded on protocol ${row.protocol}, which this version of kt cannot withdraw`);
38119
39024
  const inGroup = entriesFor(group, before.destinations);
38120
- const entry = inGroup.find((x) => x.rows.some((r) => r.id === destId));
38121
- 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}"`);
38122
39027
  const sameGroupRows = inGroup.reduce((n, x) => n + x.rows.length, 0);
38123
- 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)) {
38124
39029
  console.error(line);
38125
39030
  }
38126
39031
  console.error("");
38127
39032
  if (!await confirmTyped("Type yes to withdraw it: ")) return die("nothing was changed");
38128
39033
  const secret = await promptSecret(`current value for "${name}": `);
38129
39034
  const gone = [];
38130
- for (const r of entry.rows) {
39035
+ for (const r of entry2.rows) {
38131
39036
  try {
38132
- await client.removeCredentialDestination(vaultId, cid, r.id, secret);
39037
+ await withStepUp(client, () => client.removeCredentialDestination(vaultId, cid, r.id, secret));
38133
39038
  gone.push(r.protocol);
38134
39039
  } catch (e) {
38135
39040
  if (gone.length) {
38136
- const left = entry.rows.filter((x) => !gone.includes(x.protocol)).map((x) => PROTOCOL_LABEL[x.protocol]).join(" and ");
38137
- 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.`);
38138
39043
  }
38139
39044
  const text2 = destRefusalText(e);
38140
39045
  if (text2) return die(text2);
@@ -38143,7 +39048,7 @@ async function credDest(client, args, flags, asJson, die) {
38143
39048
  }
38144
39049
  const after = await list();
38145
39050
  if (!asJson) {
38146
- 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}`);
38147
39052
  console.log("");
38148
39053
  }
38149
39054
  return void await printList();
@@ -38155,10 +39060,37 @@ var ROLES = ["OWNER", "ADMIN", "EDITOR", "VIEWER"];
38155
39060
  var TOKEN_SCOPES = ["scim", "events"];
38156
39061
  var ORG_ROLES = ["owner", "admin", "member"];
38157
39062
  var ROTATION_SERVICES = ["linux_password", "postgres_password", "mysql_password"];
38158
- var VERSION = true ? "1.2.12" : "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
+ };
38159
39090
  var HELP = `kt \u2014 Kutrion CLI
38160
39091
  kt login [--email <e>] [--password-stdin] [--code <2fa>] prompts for the password, or reads it from stdin;
38161
- at a terminal with no --email, first asks the Kutrion desktop app,
39092
+ with no --email or --password-stdin, first asks the Kutrion desktop app
39093
+ (without a terminal, only one this CLI has already paired with),
38162
39094
  then falls back to your email and password;
38163
39095
  at the emailed-code prompt, a blank entry or r sends a new code
38164
39096
  kt login --browser not available; exits with an error
@@ -38172,8 +39104,10 @@ var HELP = `kt \u2014 Kutrion CLI
38172
39104
 
38173
39105
  kt creds <vaultId> list credentials in a vault
38174
39106
  kt get <vaultId> <credId> [--reveal] [--vault-passphrase-stdin] show a credential (the value only with --reveal); a Private vault asks for its passphrase
38175
- kt cred add <vaultId> --name <n> [--value-stdin] [--type PASSWORD] [--notes ..] prompts for the value, or reads it from stdin
38176
- 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
38177
39111
  kt cred rm <vaultId> <credId> delete a credential
38178
39112
  kt cred dest ls <vaultId> <credId> where this credential may be presented
38179
39113
  kt cred dest add <vaultId> <credId> --host <h> [--protocol SSH] [--port N]
@@ -38183,10 +39117,30 @@ var HELP = `kt \u2014 Kutrion CLI
38183
39117
  kt export <vaultId> [file] export a vault's credentials (plaintext, written mode 0600)
38184
39118
 
38185
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
38186
39125
  kt mcp status report each client's agent and which clients are wired
38187
39126
  kt mcp uninstall [--client <id>] revoke the agent and remove the client config entry
38188
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
38189
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)
38190
39144
 
38191
39145
  kt conns list SSH connections
38192
39146
  kt ssh <connId> interactive SSH shell (Ctrl-] to detach)
@@ -38198,7 +39152,10 @@ var HELP = `kt \u2014 Kutrion CLI
38198
39152
  --insert places it at the prompt without pressing Enter
38199
39153
  kt demo [start] brokered sandbox session, then the audit trail + AI summary
38200
39154
  kt demo status | reset demo availability / remove everything it provisioned
38201
- 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
38202
39159
  kt forward <connId> <localPort>:<dstHost>:<dstPort> (ssh -L style tunnel)
38203
39160
 
38204
39161
  kt sftp ls <connId> [path] list a remote directory
@@ -38225,7 +39182,8 @@ var HELP = `kt \u2014 Kutrion CLI
38225
39182
  A "side" below is written <sftp|file|cloud>:<connId>:<path> \u2014 one argument for one endpoint.
38226
39183
 
38227
39184
  kt transfer <from> <to> [--dir] remote-to-remote, brokered by the server
38228
- [--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
38229
39187
  kt sync <from> <to> [--newer-only] directory sync
38230
39188
  [--mirror] [--yes] [--dry-run] --mirror DELETES extra files at the destination
38231
39189
 
@@ -38234,6 +39192,7 @@ var HELP = `kt \u2014 Kutrion CLI
38234
39192
  kt job new <from> <to> [--wait] queue a job (--wait polls to completion)
38235
39193
  [--dir] [--verify] [--throttle N] [--newer-only] [--mirror --yes] [--timeout <sec>]
38236
39194
  kt job wait <id> [--timeout <sec>] poll an existing job
39195
+ kt job cancel <id> cancel a queued or running job
38237
39196
 
38238
39197
  kt schedule [ls] recurring transfers
38239
39198
  kt schedule new <name> <from> <to> --every <sec> or --daily-at HH:MM (UTC)
@@ -38242,6 +39201,7 @@ var HELP = `kt \u2014 Kutrion CLI
38242
39201
 
38243
39202
  kt watcher [ls] folder watchers (fire on new/changed files)
38244
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
38245
39205
  [--min-size N] [--max-size N] [--min-age <sec>] [--no-stability]
38246
39206
  kt watcher enable|disable|rm <id> pause / resume / delete a watcher
38247
39207
 
@@ -38356,7 +39316,7 @@ var HELP = `kt \u2014 Kutrion CLI
38356
39316
  [--reporter console|junit|json] [--out <file>] [--dry-run]
38357
39317
  with KT_AGENT_TOKEN set, runs as that pipeline principal over /mcp
38358
39318
 
38359
- 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
38360
39320
  [--session <id>] [--host <connection id>] [--since <iso>] [--until <iso>]
38361
39321
  kt news security news feed
38362
39322
 
@@ -38376,6 +39336,7 @@ var HELP = `kt \u2014 Kutrion CLI
38376
39336
  kt mfa verify --code <code> finish TOTP enrollment
38377
39337
  kt mfa disable turn off TOTP
38378
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
38379
39340
  kt passkeys registered passkeys / security keys
38380
39341
  kt passkey rm <id> remove a passkey
38381
39342
 
@@ -38472,14 +39433,46 @@ function runForward(base, token, connId, spec, ctx = {}) {
38472
39433
  console.log(`forwarding 127.0.0.1:${localPort} -> ${dstHost}:${dstPort} via ${connId} (Ctrl-C to stop)`);
38473
39434
  });
38474
39435
  }
39436
+ function hubCallFor(base) {
39437
+ return async (method, path, body, bearer) => {
39438
+ try {
39439
+ const res = await refuseBodyRedirects()(base + path, {
39440
+ method,
39441
+ headers: { Authorization: `Bearer ${bearer}`, Accept: "application/json", ...body === void 0 ? {} : { "Content-Type": "application/json" } },
39442
+ body: body === void 0 ? void 0 : JSON.stringify(body),
39443
+ signal: AbortSignal.timeout(15e3)
39444
+ });
39445
+ return { ok: res.ok, status: res.status, data: res.ok ? await res.json().catch(() => null) : null };
39446
+ } catch {
39447
+ return { ok: false, status: null, data: null };
39448
+ }
39449
+ };
39450
+ }
39451
+ var OWN_SIGN_IN = /* @__PURE__ */ new Set(["login", "logout", "mcp", "ci-login", "run", "enroll", "share-redeem", "help", "--help", "-h"]);
39452
+ var SESSION_REFUSED = /* @__PURE__ */ new Set(["unauthorized", "session_signed_out"]);
39453
+ var sessionWrites = trackWrites(refuseBodyRedirects());
39454
+ function refusedSavedSession(e, argv2) {
39455
+ if (!(e instanceof ApiError) || e.status !== 401 || !SESSION_REFUSED.has(refusalCode(e)) || OWN_SIGN_IN.has(argv2.cmd)) return false;
39456
+ if (Object.entries(argv2.flags).some(([k, v]) => k.endsWith("-stdin") && v === true)) return false;
39457
+ const base = resolveBase(argv2.flags.base);
39458
+ try {
39459
+ return Boolean(base && getToken(base));
39460
+ } catch {
39461
+ return false;
39462
+ }
39463
+ }
38475
39464
  async function main() {
38476
- const { cmd, positionals, flags } = parseArgs(process.argv.slice(2));
39465
+ const { cmd, positionals, flags, multi } = parseArgs(process.argv.slice(2));
38477
39466
  const asJson = flags.json === true;
38478
39467
  const die = (msg2) => {
38479
39468
  console.error(msg2);
38480
39469
  process.exit(1);
38481
39470
  };
38482
39471
  const unknown = unknownFlags(flags);
39472
+ if (flags.help === true && cmd !== "help") {
39473
+ console.log(CMD_HELP[cmd] ?? HELP);
39474
+ return;
39475
+ }
38483
39476
  if (unknown.length) die(`unknown flag: ${unknown.map((f) => `--${f}`).join(", ")}
38484
39477
  run \`kt --help\` for the flags each command takes`);
38485
39478
  if (cmd === "--version" || cmd === "-v" || cmd === "version") {
@@ -38490,6 +39483,7 @@ run \`kt --help\` for the flags each command takes`);
38490
39483
  const configured = resolveBase(flags.base);
38491
39484
  if (!configured && !offlineLint && cmd !== "help" && cmd !== "--help" && cmd !== "-h") die(NO_BASE);
38492
39485
  const cfg = { base: configured };
39486
+ if (!offlineLint && configured) installBaseFallback(configured);
38493
39487
  let issued;
38494
39488
  let signedThisRun = false;
38495
39489
  const ensureRefreshKeyBound = async () => {
@@ -38515,8 +39509,11 @@ run \`kt --help\` for the flags each command takes`);
38515
39509
  });
38516
39510
  if (outcome !== "failed" && borrowedDevice(cfg.base, kcSlots)?.id === dev.id) settleRefreshBind(cfg.base, kcSlots, dev.id);
38517
39511
  };
39512
+ const longCall = cmd === "transfer" || cmd === "sync";
39513
+ const longTimeoutSec = Number(flags.timeout);
38518
39514
  const client = new KutrionClient({
38519
39515
  baseUrl: cfg.base,
39516
+ ...longCall ? { timeoutMs: (Number.isFinite(longTimeoutSec) && longTimeoutSec > 0 ? longTimeoutSec : 6 * 3600) * 1e3 } : {},
38520
39517
  getToken: () => getToken(cfg.base),
38521
39518
  onToken: (t) => {
38522
39519
  issued = t || void 0;
@@ -38527,7 +39524,7 @@ run \`kt --help\` for the flags each command takes`);
38527
39524
  // blank User-Agent it never set. Relaxing only, and never required — an older CLI that omits it
38528
39525
  // is treated exactly as it is today.
38529
39526
  client: "cli",
38530
- fetchImpl: refuseBodyRedirects(),
39527
+ fetchImpl: sessionWrites.fetch,
38531
39528
  // #2984: only a hub borrow leaves a refresh token, in the OS keychain beside the access token.
38532
39529
  getRefreshToken: () => {
38533
39530
  try {
@@ -38562,17 +39559,13 @@ run \`kt --help\` for the flags each command takes`);
38562
39559
  }
38563
39560
  });
38564
39561
  const kc = { get: getToken, set: setToken, clear: clearToken };
38565
- const hubCall = async (method, path, body, bearer) => {
39562
+ const hubCall = hubCallFor(cfg.base);
39563
+ const vaultCustody = async (vaultId) => {
38566
39564
  try {
38567
- const res = await refuseBodyRedirects()(cfg.base + path, {
38568
- method,
38569
- headers: { Authorization: `Bearer ${bearer}`, Accept: "application/json", ...body === void 0 ? {} : { "Content-Type": "application/json" } },
38570
- body: body === void 0 ? void 0 : JSON.stringify(body),
38571
- signal: AbortSignal.timeout(15e3)
38572
- });
38573
- return { ok: res.ok, status: res.status, data: res.ok ? await res.json().catch(() => null) : null };
38574
- } catch {
38575
- return { ok: false, status: null, data: null };
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;
38576
39569
  }
38577
39570
  };
38578
39571
  const connectionContext = async (connId) => {
@@ -38650,6 +39643,10 @@ run \`kt --help\` for the flags each command takes`);
38650
39643
  timeoutMs: (intFlag("timeout") ?? 3600) * 1e3,
38651
39644
  onTick: (j) => {
38652
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
+ `);
38653
39650
  }
38654
39651
  });
38655
39652
  if (ticker) process.stderr.write("\n");
@@ -38659,8 +39656,8 @@ run \`kt --help\` for the flags each command takes`);
38659
39656
  process.exit(EXIT_UNKNOWN);
38660
39657
  }
38661
39658
  show(job);
38662
- if (job.status === "failed") {
38663
- 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"}`);
38664
39661
  process.exit(EXIT_FAIL);
38665
39662
  }
38666
39663
  process.exit(EXIT_OK);
@@ -38684,7 +39681,8 @@ run \`kt --help\` for the flags each command takes`);
38684
39681
  console.error("kt login --browser is not available: browser sign-in is switched off. Sign in with the Kutrion app (`kt login`), or use `kt login --email <email> --password-stdin`.");
38685
39682
  process.exit(EXIT_ERROR);
38686
39683
  }
38687
- if (process.stdin.isTTY && flags.email === void 0 && !fromStdin && flags.code === void 0 && accountRoot) {
39684
+ const tty = canPromptYes(process.stdin, process.stderr);
39685
+ if (accountRoot && loginTriesHub(flags, accountRoot)) {
38688
39686
  const hub = await hubLogin({
38689
39687
  base: cfg.base,
38690
39688
  location: hubLocation(accountRoot),
@@ -38692,7 +39690,8 @@ run \`kt --help\` for the flags each command takes`);
38692
39690
  keychainAvailable: osKeychainAvailable,
38693
39691
  call: hubCall,
38694
39692
  out: (l) => console.error(l),
38695
- ask: (q) => promptLine(q)
39693
+ ask: (q) => promptLine(q),
39694
+ pairedOnly: !tty
38696
39695
  });
38697
39696
  if (hub.kind === "store_failed") die(hub.note);
38698
39697
  if (hub.kind === "signed_in") {
@@ -38702,7 +39701,7 @@ run \`kt --help\` for the flags each command takes`);
38702
39701
  break;
38703
39702
  }
38704
39703
  if (hub.note) console.error(hub.note);
38705
- if (hubAbsent(hub.reason)) console.error("Installing the Kutrion app... Run `kt login` again once it is open, or sign in with your email and password below.");
39704
+ if (tty && hubAbsent(hub.reason)) console.error("Installing the Kutrion app... Run `kt login` again once it is open, or sign in with your email and password below.");
38706
39705
  }
38707
39706
  if (flags.email === void 0 && !fromStdin) {
38708
39707
  const hint = otherAppsHint(accountRoot);
@@ -38744,7 +39743,6 @@ run \`kt --help\` for the flags each command takes`);
38744
39743
  process.exit(1);
38745
39744
  }
38746
39745
  const staleDevice = dropBorrowed(cfg.base, kc);
38747
- if (staleDevice) await client.revokeDevice(staleDevice).catch(() => void 0);
38748
39746
  const post = fetchPost(cfg.base, refuseBodyRedirects());
38749
39747
  const registered = await registerOwnDevice({
38750
39748
  base: cfg.base,
@@ -38759,9 +39757,12 @@ run \`kt --help\` for the flags each command takes`);
38759
39757
  } catch {
38760
39758
  return null;
38761
39759
  }
38762
- })()
39760
+ })(),
39761
+ previousDeviceId: staleDevice
38763
39762
  }).catch(() => ({ kind: "skipped", note: null }));
39763
+ if (staleDevice) await client.revokeDevice(staleDevice).catch(() => void 0);
38764
39764
  recordSignIn(accountRoot, signedIn);
39765
+ recordLastAccountId(cfg.base, kc, signedIn);
38765
39766
  console.log(`logged in (token stored in ${backendName()})`);
38766
39767
  if (rememberBase(cfg.base)) console.log(`server saved: ${cfg.base} (later commands need no --base)`);
38767
39768
  if (registered.kind === "skipped" && registered.note) console.error(registered.note);
@@ -38790,7 +39791,10 @@ run \`kt --help\` for the flags each command takes`);
38790
39791
  break;
38791
39792
  case "whoami":
38792
39793
  requireAuth();
38793
- show((await client.me()).user);
39794
+ {
39795
+ const { user } = await client.me();
39796
+ show(asJson ? user : whoamiLines(user));
39797
+ }
38794
39798
  break;
38795
39799
  case "vaults":
38796
39800
  requireAuth();
@@ -38799,8 +39803,13 @@ run \`kt --help\` for the flags each command takes`);
38799
39803
  case "vault": {
38800
39804
  requireAuth();
38801
39805
  const sub = positionals[0];
38802
- if (sub === "create" && positionals[1]) show((await client.createVault(positionals.slice(1).join(" "))).vault);
38803
- 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);
38804
39813
  else if (sub === "rm" && positionals[1]) {
38805
39814
  await client.deleteVault(positionals[1]);
38806
39815
  console.log("deleted");
@@ -38829,12 +39838,15 @@ run \`kt --help\` for the flags each command takes`);
38829
39838
  noReveal = credential.value === null;
38830
39839
  } else {
38831
39840
  const summary = (await client.listCredentials(positionals[0])).credentials.find((c) => c.id === positionals[1]);
38832
- 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
+ }
38833
39845
  noReveal = summary.inject_only === true;
38834
39846
  if (noReveal) show({ ...summary, value: null });
38835
39847
  else {
38836
39848
  show({ ...summary, value_hidden: true });
38837
- console.error("value hidden; pass --reveal to print it");
39849
+ if (!asJson) console.error("value hidden; pass --reveal to print it");
38838
39850
  }
38839
39851
  }
38840
39852
  if (noReveal) {
@@ -38856,17 +39868,45 @@ run \`kt --help\` for the flags each command takes`);
38856
39868
  if (flags.value !== void 0) refuseSecretFlag("value", "pipe it to --value-stdin, or leave it off to be prompted");
38857
39869
  if (flags["value-stdin"] === true && typeof flags.name !== "string") die("--value-stdin needs --name, because stdin carries the value");
38858
39870
  const name = String(flags.name ?? await prompt("name: "));
38859
- const value = flags["value-stdin"] === true ? await readSecret("value: ") : await prompt("value: ", true);
38860
39871
  const notes = flags.notes !== void 0 ? String(flags.notes) : void 0;
38861
- 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));
38862
39886
  } else if (sub === "set" && vaultId && positionals[2]) {
38863
39887
  if (flags.value !== void 0) refuseSecretFlag("value", "pipe it to --value-stdin");
38864
39888
  const patch = {};
38865
39889
  if (flags.name !== void 0) patch.name = String(flags.name);
38866
- if (flags["value-stdin"] === true) patch.value = await readSecret("value: ");
38867
39890
  if (flags.notes !== void 0) patch.notes = String(flags.notes);
38868
- if (!Object.keys(patch).length) return die("nothing to update \u2014 pass --name/--value-stdin/--notes");
38869
- 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));
38870
39910
  } else if (sub === "rm" && vaultId && positionals[2]) {
38871
39911
  await client.deleteCredential(vaultId, positionals[2]);
38872
39912
  console.log("deleted");
@@ -38947,8 +39987,20 @@ run \`kt --help\` for the flags each command takes`);
38947
39987
  }
38948
39988
  case "exec": {
38949
39989
  requireAuth();
38950
- if (!positionals[0] || !positionals[1]) return die("usage: kt exec <connId> <command...>");
38951
- 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) => {
38952
40004
  const k = hostKeyUnconfirmedFromError(e);
38953
40005
  const chained = k ? null : hostKeyChainedFromError(e);
38954
40006
  if (!k && !chained) throw e;
@@ -38987,7 +40039,7 @@ run \`kt --help\` for the flags each command takes`);
38987
40039
  }
38988
40040
  let job;
38989
40041
  try {
38990
- job = parseJobFile((0, import_fs5.readFileSync)(path, "utf8"));
40042
+ job = parseJobFile((0, import_fs7.readFileSync)(path, "utf8"));
38991
40043
  } catch (e) {
38992
40044
  return die(e instanceof JobFileError ? `${path}: ${e.message}` : `${path}: ${e.message}`);
38993
40045
  }
@@ -38996,7 +40048,7 @@ run \`kt --help\` for the flags each command takes`);
38996
40048
  if (agentToken && !asJson && reporter === "console") {
38997
40049
  console.error(`running as a pipeline principal through ${cfg.base.replace(/\/+$/, "")}/mcp`);
38998
40050
  }
38999
- 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) }, {
39000
40052
  dryRun,
39001
40053
  onStep: (s) => {
39002
40054
  if (asJson || reporter !== "console") return;
@@ -39022,7 +40074,7 @@ run \`kt --help\` for the flags each command takes`);
39022
40074
  const path = positionals[2] ?? ".";
39023
40075
  const r = await client.sftpList(connId, path);
39024
40076
  if (asJson) return console.log(JSON.stringify(r, null, 2));
39025
- show(r.entries, ["name", "type", "size", "mode"]);
40077
+ show(r.entries.map((e) => ({ ...e, mode: modeString(e.mode, e.type) })), ["name", "type", "size", "mode"]);
39026
40078
  } else if (sub === "mkdir" && connId && positionals[2]) {
39027
40079
  await client.sftpMkdir(connId, positionals[2]);
39028
40080
  console.log("created");
@@ -39094,6 +40146,11 @@ run \`kt --help\` for the flags each command takes`);
39094
40146
  to,
39095
40147
  throttle_bps: intFlag("throttle"),
39096
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);
39097
40154
  });
39098
40155
  if (asJson) return console.log(JSON.stringify(r, null, 2));
39099
40156
  console.log(`transferred ${r.files} file(s), ${fmtBytes(r.bytes)}${r.skipped ? `, ${r.skipped} skipped` : ""}${r.renamed ? `, ${r.renamed} renamed` : ""}`);
@@ -39142,8 +40199,10 @@ run \`kt --help\` for the flags each command takes`);
39142
40199
  return await watchJob(job.id);
39143
40200
  } else if (sub === "wait" && positionals[1]) {
39144
40201
  return await watchJob(positionals[1]);
40202
+ } else if (sub === "cancel" && positionals[1]) {
40203
+ show((await client.cancelTransferJob(positionals[1])).job);
39145
40204
  } else {
39146
- 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>");
39147
40206
  }
39148
40207
  break;
39149
40208
  }
@@ -39193,6 +40252,13 @@ run \`kt --help\` for the flags each command takes`);
39193
40252
  } else if (sub === "new" && positionals[1] && positionals[2] && positionals[3]) {
39194
40253
  const source = sideArg(positionals[2], "source");
39195
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
+ }
39196
40262
  show((await client.createFolderWatcher({
39197
40263
  name: positionals[1],
39198
40264
  source,
@@ -39206,7 +40272,7 @@ run \`kt --help\` for the flags each command takes`);
39206
40272
  // Stability checks are what stop a watcher picking up a file that is still being written.
39207
40273
  // Off is opt-in, never a default.
39208
40274
  stability_checks: flags["no-stability"] !== true,
39209
- interval_seconds: intFlag("every")
40275
+ interval_seconds: every
39210
40276
  })).watcher);
39211
40277
  } else if ((sub === "enable" || sub === "disable") && positionals[1]) {
39212
40278
  await client.setFolderWatcherEnabled(positionals[1], sub === "enable");
@@ -39278,11 +40344,11 @@ ${r.authorize_url}`);
39278
40344
  const buf = Buffer.from(await blob.arrayBuffer());
39279
40345
  if (local === "-") process.stdout.write(buf);
39280
40346
  else {
39281
- (0, import_fs5.writeFileSync)(local, buf);
40347
+ (0, import_fs7.writeFileSync)(local, buf);
39282
40348
  console.error(`saved ${local} (${fmtBytes(buf.length)})`);
39283
40349
  }
39284
40350
  } else if (sub === "put" && id && positionals[2] && positionals[3]) {
39285
- const buf = (0, import_fs5.readFileSync)(positionals[2]);
40351
+ const buf = (0, import_fs7.readFileSync)(positionals[2]);
39286
40352
  await client.cloudUpload(id, positionals[3], new Blob([buf]));
39287
40353
  console.error(`uploaded ${fmtBytes(buf.length)}`);
39288
40354
  } else if (sub === "mkdir" && id && positionals[2]) {
@@ -39388,17 +40454,20 @@ ${r.token}`);
39388
40454
  }
39389
40455
  const scopes = flags.scopes ? String(flags.scopes).split(",").map((s) => s.trim()).filter(Boolean) : DEFAULT_SCOPES;
39390
40456
  const ttlDays = flags.days !== void 0 ? Number(flags.days) : DEFAULT_TTL_DAYS;
39391
- const dir = (0, import_path6.resolve)(String(flags.dir ?? process.cwd()));
40457
+ const dir = (0, import_path7.resolve)(String(flags.dir ?? process.cwd()));
39392
40458
  const egress = String(flags.egress ?? "cloud");
39393
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)");
39394
40461
  try {
39395
- 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 });
39396
40464
  warnLeftLive(r, client2);
39397
40465
  if (asJson) return console.log(JSON.stringify(r, null, 2));
39398
40466
  console.log(`installed ${CLIENTS[client2].label}`);
39399
40467
  console.log(` config ${r.configPath}`);
39400
40468
  console.log(` agent ${r.principalId} (expires ${r.expiresAt})`);
39401
40469
  console.log(` scopes ${r.scopes.join(", ")}`);
40470
+ console.log(` reach ${describeScope(r.resourceScope)}`);
39402
40471
  if (r.superseded) console.log(` revoked ${r.superseded} earlier agent(s) for this client`);
39403
40472
  console.log(` credential stored in the ${backendName()} \u2014 the config file does not contain it`);
39404
40473
  console.log(r.verified ? " verified connection to the broker works" : " warning: could not verify the connection");
@@ -39417,7 +40486,7 @@ next: ${r.next}`);
39417
40486
  break;
39418
40487
  }
39419
40488
  if (sub === "status") {
39420
- const dir = (0, import_path6.resolve)(String(flags.dir ?? process.cwd()));
40489
+ const dir = (0, import_path7.resolve)(String(flags.dir ?? process.cwd()));
39421
40490
  const s = await mcpStatus(cfg.base, dir);
39422
40491
  if (asJson) return console.log(JSON.stringify(s, null, 2));
39423
40492
  console.log(`credential store: ${s.keychain}`);
@@ -39437,6 +40506,7 @@ next: ${r.next}`);
39437
40506
  if (c.agent?.state !== "live") continue;
39438
40507
  const rl = c.agent.rate_limit;
39439
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)}`);
39440
40510
  }
39441
40511
  if (s.legacy) console.log(`
39442
40512
  shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.trimEnd());
@@ -39448,7 +40518,7 @@ shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.
39448
40518
  if (client2 && !CLIENT_IDS.includes(client2)) {
39449
40519
  return die(`unknown --client; one of: ${CLIENT_IDS.join(", ")}`);
39450
40520
  }
39451
- const dir = (0, import_path6.resolve)(String(flags.dir ?? process.cwd()));
40521
+ const dir = (0, import_path7.resolve)(String(flags.dir ?? process.cwd()));
39452
40522
  const r = await uninstallClient({ base: cfg.base, userToken, client: client2, dir });
39453
40523
  warnLeftLive(r, client2);
39454
40524
  if (asJson) return console.log(JSON.stringify(r, null, 2));
@@ -39457,7 +40527,7 @@ shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.
39457
40527
  break;
39458
40528
  }
39459
40529
  if (sub && sub !== "serve") {
39460
- 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>]");
39461
40531
  }
39462
40532
  if (flags.token !== void 0) refuseSecretFlag("token", "export KT_AGENT_TOKEN instead, or run kt mcp install");
39463
40533
  let agentToken;
@@ -39478,6 +40548,136 @@ shared pre-split credential: ${s.legacy.principal_id ?? ""} ${state(s.legacy)}`.
39478
40548
  await runMcpBridge({ base: cfg.base, token: agentToken });
39479
40549
  break;
39480
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
+ }
39481
40681
  // #674: exchange this CI job's own OIDC token for a short-lived agent principal. No Kutrion
39482
40682
  // secret is stored in the pipeline — the job token is minted per run by the CI provider.
39483
40683
  case "ci-login": {
@@ -39693,7 +40893,15 @@ ${snippetPreview(s)}
39693
40893
  const connection_id = String(flags.conn ?? ""), credential_id = String(flags.cred ?? "");
39694
40894
  if (!connection_id || !credential_id) return die("need --conn <id> --cred <id>");
39695
40895
  const input = { connection_id, credential_id, service, target: flags.target ? String(flags.target) : void 0 };
39696
- 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
+ }
39697
40905
  } else die("usage: kt rotation services | history <credId> | plan|run --conn <id> --cred <id> --service <svc> [--target ..]");
39698
40906
  break;
39699
40907
  }
@@ -39765,8 +40973,9 @@ ${owed}`);
39765
40973
  case "device":
39766
40974
  requireAuth();
39767
40975
  if (positionals[0] === "rm" && positionals[1]) {
39768
- await client.revokeDevice(positionals[1]);
40976
+ const r = await client.revokeDevice(positionals[1]);
39769
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(", ")}`);
39770
40979
  } else die("usage: kt device rm <deviceId>");
39771
40980
  break;
39772
40981
  // --- vault share ---
@@ -39829,7 +41038,7 @@ ${r.url}`);
39829
41038
  } else if (sub === "import") {
39830
41039
  const vaultId = positionals[1], file = positionals[2];
39831
41040
  if (!vaultId || !file) return die(CONN_IMPORT_USAGE);
39832
- const parsed = await client.parseConnImport((0, import_fs5.readFileSync)(file, "utf8"), {
41041
+ const parsed = await client.parseConnImport((0, import_fs7.readFileSync)(file, "utf8"), {
39833
41042
  provider: flags.provider ? String(flags.provider) : void 0,
39834
41043
  filename: file.split("/").pop()
39835
41044
  });
@@ -39959,7 +41168,7 @@ ${ENROLL_USAGE}`);
39959
41168
  const confirms = parseConfirms(process.argv.slice(2));
39960
41169
  if (!(confirms instanceof Map)) return die(confirms.error);
39961
41170
  if (flags.key === true || flags["known-hosts"] === true || flags.vault === true) return die(ENROLL_USAGE);
39962
- 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;
39963
41172
  const fallback = flags.fallback === true;
39964
41173
  const vaultId = typeof flags.vault === "string" ? flags.vault : void 0;
39965
41174
  const timeoutSec = intFlag("timeout") ?? 1800;
@@ -39979,7 +41188,7 @@ ${r.label} ${r.host.observedFingerprint}`);
39979
41188
  if (!chosen.length) return die(ENROLL_COPY.noneConfirmed);
39980
41189
  let bootstrap;
39981
41190
  if (typeof flags.key === "string") {
39982
- const privateKey = (0, import_fs5.readFileSync)(flags.key, "utf8");
41191
+ const privateKey = (0, import_fs7.readFileSync)(flags.key, "utf8");
39983
41192
  let passphrase;
39984
41193
  if (isEncryptedKey(privateKey)) {
39985
41194
  if (flags["password-stdin"] === true) passphrase = await readStdin();
@@ -40172,14 +41381,15 @@ ${BROKER_CA_USAGE}`);
40172
41381
  break;
40173
41382
  case "import-providers":
40174
41383
  requireAuth();
40175
- 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"]);
40176
41385
  break;
40177
41386
  case "import": {
40178
41387
  requireAuth();
40179
41388
  const vaultId = positionals[0];
40180
41389
  const file = positionals[1];
40181
41390
  if (!vaultId || !file) die("usage: kt import <vaultId> <file> [--provider <name>] (see: kt import-providers)");
40182
- 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);
40183
41393
  const parsed = await client.parseImport(content, { provider: flags.provider ? String(flags.provider) : void 0, filename: file.split("/").pop() });
40184
41394
  for (const w of parsed.warnings) console.error("warning:", w);
40185
41395
  const items = parsed.items.filter((it) => it.name && it.value).map((it) => ({ ...it, name: it.name.slice(0, 255) }));
@@ -40258,6 +41468,19 @@ Add this to your authenticator, then run: kt mfa verify --code <code>`);
40258
41468
  }
40259
41469
  break;
40260
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
+ }
40261
41484
  case "passkeys":
40262
41485
  requireAuth();
40263
41486
  show((await client.listPasskeys()).passkeys, ["id", "label", "kind", "created_at", "last_used"]);
@@ -40287,12 +41510,36 @@ run \`kt --help\` for the list of commands`);
40287
41510
  }
40288
41511
  }
40289
41512
  installFetchProxy();
40290
- main().catch((e) => {
41513
+ var argv = parseArgs(process.argv.slice(2));
41514
+ var hubSlots = { get: getToken, set: setToken, clear: clearToken };
41515
+ var hubSignInAvailable = (e) => e instanceof ApiError && e.status === 401 && appDataDir() !== null && osKeychainAvailable();
41516
+ retryAfterHubReborrow(main, {
41517
+ refusedSession: (e) => refusedSavedSession(e, argv),
41518
+ wrote: sessionWrites.wrote,
41519
+ lastAccount: () => lastAccountId(resolveBase(argv.flags.base), hubSlots),
41520
+ reborrow: async () => {
41521
+ const base = resolveBase(argv.flags.base);
41522
+ const root = appDataDir();
41523
+ if (!root) return { kind: "fallback", note: null, reason: "hub_not_running" };
41524
+ const hub = await reborrowFromHub({
41525
+ base,
41526
+ location: hubLocation(root),
41527
+ kc: hubSlots,
41528
+ keychainAvailable: osKeychainAvailable,
41529
+ call: hubCallFor(base),
41530
+ out: (l) => console.error(l),
41531
+ ask: (q) => promptLine(q)
41532
+ });
41533
+ if (hub.kind === "signed_in") recordSignIn(root, hub.user);
41534
+ return hub.kind === "fallback" && hubAbsent(hub.reason) ? { ...hub, note: null } : hub;
41535
+ },
41536
+ out: (l) => console.error(l)
41537
+ }).catch((e) => {
40291
41538
  const api3 = e instanceof ApiError ? e : null;
40292
41539
  const code = api3 ? exitForStatus(api3.status) : EXIT_ERROR;
40293
41540
  const message = redactProxyUrls(e instanceof Error ? e.message : String(e));
40294
- const lines = (storeFailure(e) ?? refusalLines(e) ?? [`error: ${message}`]).map(redactProxyUrls);
40295
- if (parseArgs(process.argv.slice(2)).flags.json === true) {
41541
+ const lines = (e instanceof HubAccountSwitchedError ? [e.message] : storeFailure(e) ?? refusalLines(e, " ", { hubSignIn: hubSignInAvailable(e) }) ?? [`error: ${message}`]).map(redactProxyUrls);
41542
+ if (argv.flags.json === true) {
40296
41543
  console.log(JSON.stringify({
40297
41544
  error: api3 ? refusalCode(api3) : message,
40298
41545
  ...api3 ? { status: api3.status } : {},