@kontextmind/kxm 0.7.167 → 0.7.168

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.
@@ -556,6 +556,9 @@ function resolveUserStateRoot(env) {
556
556
  function hubBindingFile(env = process.env) {
557
557
  return join3(resolveUserStateRoot(env), "hub-binding.json");
558
558
  }
559
+ function hubBindingCloudFile(env = process.env) {
560
+ return join3(resolveUserStateRoot(env), "hub-binding.cloud.json");
561
+ }
559
562
  function validateHubUrl(raw) {
560
563
  let parsed;
561
564
  try {
@@ -618,33 +621,35 @@ function isAbortError(error) {
618
621
  error && typeof error === "object" && ("name" in error && error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR")
619
622
  );
620
623
  }
621
- function readHubBinding(env = process.env) {
622
- const file = hubBindingFile(env);
623
- if (!existsSync4(file)) return void 0;
624
+ function bindingFallbackWarning(file, reason) {
625
+ return `hub binding at ${file} ${reason}; falling back to the local hub. Restart the Runtime supervisor, the hub, and workers after upgrading and before a cloud bind.`;
626
+ }
627
+ function writeBindingJson(file, value) {
628
+ mkdirSync3(dirname(file), { recursive: true, mode: 448 });
629
+ const temporary = join3(dirname(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
630
+ try {
631
+ writeFileSync2(temporary, `${JSON.stringify(value, null, 2)}
632
+ `, { encoding: "utf8", mode: 384 });
633
+ renameSync(temporary, file);
634
+ } finally {
635
+ rmSync(temporary, { force: true });
636
+ }
637
+ }
638
+ function parseBindingObject(file) {
639
+ if (!existsSync4(file)) return new HubBindingError(`missing hub binding at ${file}`);
624
640
  let parsed;
625
641
  try {
626
642
  parsed = JSON.parse(readFileSync4(file, "utf8"));
627
643
  } catch {
628
- throw new HubBindingError(`malformed hub binding at ${file}`);
644
+ return new HubBindingError(`malformed hub binding at ${file}`);
629
645
  }
630
646
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
631
- throw new HubBindingError(`malformed hub binding at ${file}`);
632
- }
633
- const row = parsed;
634
- const keys = Object.keys(row);
635
- const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
636
- const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
637
- if (keys.some((key) => !allowed.has(key)) || !cloudish && keys.length !== 3 || row.schema !== HUB_BINDING_SCHEMA || typeof row.url !== "string" || typeof row.boundAt !== "string" || !isIsoTimestamp(row.boundAt)) {
638
- throw new HubBindingError(`malformed hub binding at ${file}`);
639
- }
640
- let url;
641
- try {
642
- url = validateHubUrl(row.url);
643
- } catch {
644
- throw new HubBindingError(`malformed hub binding at ${file}`);
647
+ return new HubBindingError(`malformed hub binding at ${file}`);
645
648
  }
646
- if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
647
- if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
649
+ return parsed;
650
+ }
651
+ function cloudRecordFromRow(file, row, url, boundAt) {
652
+ if (row.cloud !== true) return void 0;
648
653
  let tokenEnv;
649
654
  let tokenCommand;
650
655
  if (row.tokenEnv !== void 0) {
@@ -666,36 +671,163 @@ function readHubBinding(env = process.env) {
666
671
  return {
667
672
  schema: HUB_BINDING_SCHEMA,
668
673
  url,
669
- boundAt: row.boundAt,
674
+ boundAt,
670
675
  cloud: true,
671
676
  ...tokenEnv ? { tokenEnv } : {},
672
677
  ...tokenCommand ? { tokenCommand } : {}
673
678
  };
674
679
  }
680
+ function interpretBindingFile(file) {
681
+ if (!existsSync4(file)) return {};
682
+ const parsed = parseBindingObject(file);
683
+ if (parsed instanceof HubBindingError) throw parsed;
684
+ const keys = Object.keys(parsed);
685
+ const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
686
+ if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
687
+ return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
688
+ }
689
+ if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
690
+ throw new HubBindingError(`malformed hub binding at ${file}`);
691
+ }
692
+ let url;
693
+ try {
694
+ url = validateHubUrl(parsed.url);
695
+ } catch {
696
+ throw new HubBindingError(`malformed hub binding at ${file}`);
697
+ }
698
+ const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
699
+ if (!cloudish) {
700
+ if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
701
+ return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
702
+ }
703
+ const record = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
704
+ if (!record) throw new HubBindingError(`malformed hub binding at ${file}`);
705
+ return { record, legacyCloud: true };
706
+ }
707
+ function loadHubBinding(env = process.env) {
708
+ const cloudFile = hubBindingCloudFile(env);
709
+ const cloud = interpretBindingFile(cloudFile);
710
+ if (cloud.warning && !cloud.record) {
711
+ const local = existsSync4(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
712
+ if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
713
+ return { warning: cloud.warning };
714
+ }
715
+ if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
716
+ const main2 = interpretBindingFile(hubBindingFile(env));
717
+ if (main2.legacyCloud && main2.record) {
718
+ writeBindingJson(cloudFile, main2.record);
719
+ rmSync(hubBindingFile(env), { force: true });
720
+ return {
721
+ record: main2.record,
722
+ warning: bindingFallbackWarning(
723
+ hubBindingFile(env),
724
+ "stored a cloud bind in the pre-0.7.160 file"
725
+ )
726
+ };
727
+ }
728
+ return { ...main2.record ? { record: main2.record } : {}, ...main2.warning ? { warning: main2.warning } : {} };
729
+ }
730
+ function readHubBinding(env = process.env) {
731
+ return loadHubBinding(env).record;
732
+ }
733
+ function contentTypeBase(header) {
734
+ return (header ?? "").split(";")[0].trim().toLowerCase();
735
+ }
736
+ function looksLikeHtml(contentType, body) {
737
+ if (contentType === "text/html" || contentType === "application/xhtml+xml") return true;
738
+ const sample = body.slice(0, 800).trim().toLowerCase();
739
+ return sample.startsWith("<!doctype html") || sample.startsWith("<html") || sample.includes("<form") && (sample.includes("login") || sample.includes("authentik") || sample.includes("password"));
740
+ }
741
+ function jsonObject(text) {
742
+ try {
743
+ const parsed = JSON.parse(text);
744
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
745
+ return parsed;
746
+ } catch {
747
+ return void 0;
748
+ }
749
+ }
750
+ function hubEndpointShape(role, body) {
751
+ if (role === "health") return body.ok === true && typeof body.agents === "number";
752
+ return body.ok === true && (body.storage === "sqlite" || body.storage === "memory");
753
+ }
754
+ function classifyHubResponse(input) {
755
+ const contentType = contentTypeBase(input.contentType);
756
+ if (input.status >= 300 && input.status < 400) {
757
+ return {
758
+ ok: false,
759
+ status: input.status,
760
+ kind: "redirect",
761
+ detail: `HTTP ${input.status} redirect; this looks like an auth proxy or a redirect, not a kxm hub`,
762
+ body: { error: "auth_proxy", detail: `HTTP ${input.status} redirect` }
763
+ };
764
+ }
765
+ if (looksLikeHtml(contentType, input.bodyText)) {
766
+ return {
767
+ ok: false,
768
+ status: input.status,
769
+ kind: "auth_proxy",
770
+ detail: `HTTP ${input.status} returned an HTML login page${contentType ? ` (${contentType})` : ""}; this looks like an auth proxy, not a kxm hub`,
771
+ body: { error: "auth_proxy", detail: "HTML login page" }
772
+ };
773
+ }
774
+ const jsonType = contentType === "application/json" || contentType === "text/plain" || contentType === "";
775
+ const body = jsonType ? jsonObject(input.bodyText) : void 0;
776
+ if (body && hubEndpointShape(input.role, body) && input.status >= 200 && input.status < 300) {
777
+ if (contentType !== "" && contentType !== "application/json" && contentType !== "text/plain") {
778
+ return {
779
+ ok: false,
780
+ status: input.status,
781
+ kind: "not_hub",
782
+ detail: `HTTP ${input.status} content-type ${contentType} is not application/json`,
783
+ body: { error: "not_hub", detail: contentType }
784
+ };
785
+ }
786
+ return { ok: true, status: input.status, kind: "hub", detail: "kxm hub", body };
787
+ }
788
+ const typeNote = contentType && contentType !== "application/json" ? ` content-type ${contentType}` : "";
789
+ return {
790
+ ok: false,
791
+ status: input.status,
792
+ kind: "not_hub",
793
+ detail: `HTTP ${input.status}${typeNote} is not a kxm hub ${input.role} response`,
794
+ body: { error: "not_hub", detail: `HTTP ${input.status}${typeNote}` }
795
+ };
796
+ }
797
+ async function readHubEndpoint(url, role, fetchImpl, init = {}) {
798
+ try {
799
+ const response = await fetchImpl(`${url}/${role}`, {
800
+ ...init.headers ? { headers: init.headers } : {},
801
+ ...init.signal ? { signal: init.signal } : {},
802
+ redirect: "manual"
803
+ });
804
+ const bodyText = await response.text();
805
+ return classifyHubResponse({
806
+ role,
807
+ status: response.status,
808
+ contentType: response.headers.get("content-type"),
809
+ bodyText
810
+ });
811
+ } catch (error) {
812
+ if (isAbortError(error)) {
813
+ return { ok: false, status: 0, kind: "timeout", detail: "no reply within the probe budget", body: { error: "hub_unreachable" } };
814
+ }
815
+ return { ok: false, status: 0, kind: "unreachable", detail: "nothing answered", body: { error: "hub_unreachable" } };
816
+ }
817
+ }
675
818
  async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
676
819
  const started = Date.now();
677
820
  const controller = new AbortController();
678
821
  const timer = setTimeout(() => controller.abort(), timeoutMs);
679
822
  try {
680
- const response = await fetchImpl(`${url}/health`, { signal: controller.signal });
681
- if (!response.ok) return { health: "unknown", probeMs: Date.now() - started };
682
- let body;
683
- try {
684
- body = await response.json();
685
- } catch {
686
- return { health: "unknown", probeMs: Date.now() - started };
687
- }
688
- if (body && typeof body === "object" && body.ok === true) {
689
- return { health: "on", probeMs: Date.now() - started };
690
- }
691
- return { health: "unknown", probeMs: Date.now() - started };
692
- } catch (error) {
693
- return { health: isAbortError(error) ? "unknown" : "off", probeMs: Date.now() - started };
823
+ const verdict = await readHubEndpoint(url, "health", fetchImpl, { signal: controller.signal });
824
+ const health = verdict.kind === "hub" ? "on" : verdict.kind === "unreachable" ? "off" : "unknown";
825
+ return { health, probeMs: Date.now() - started, kind: verdict.kind, detail: verdict.detail, status: verdict.status };
694
826
  } finally {
695
827
  clearTimeout(timer);
696
828
  }
697
829
  }
698
- var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError;
830
+ var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError, LEGACY_BINDING_KEYS, BINDING_KEYS;
699
831
  var init_hub_binding = __esm({
700
832
  "plugins/kxm/src/hub-binding.ts"() {
701
833
  "use strict";
@@ -716,6 +848,8 @@ var init_hub_binding = __esm({
716
848
  this.name = "HubBindingError";
717
849
  }
718
850
  };
851
+ LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
852
+ BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
719
853
  }
720
854
  });
721
855