@calimero-network/mero-react 4.0.1 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -268,14 +268,19 @@ function MeroProvider({
268
268
  },
269
269
  [timeoutMs, tokenStore]
270
270
  );
271
- const checkAuth = react.useCallback(async (instance) => {
272
- try {
273
- await instance.admin.getContexts();
274
- return true;
275
- } catch {
276
- return false;
277
- }
278
- }, []);
271
+ const checkAuth = react.useCallback(
272
+ async (instance) => {
273
+ const accessToken = tokenStore.getTokens()?.access_token;
274
+ if (!accessToken) return false;
275
+ try {
276
+ const { valid } = await instance.auth.validateToken(accessToken);
277
+ return valid;
278
+ } catch {
279
+ return false;
280
+ }
281
+ },
282
+ [tokenStore]
283
+ );
279
284
  const connectToNode = react.useCallback(
280
285
  (url) => {
281
286
  if (!isBrowser) return;
@@ -473,6 +478,59 @@ function CalimeroLogo({
473
478
  );
474
479
  }
475
480
 
481
+ // src/utils/nodeDiscovery.ts
482
+ var DEFAULT_LOCAL_NODE_PORTS = [2428, 2429, 2528, 2529];
483
+ var LOCAL_HOST = "localhost";
484
+ var DEFAULT_PROBE_TIMEOUT_MS = 2e3;
485
+ function localNodeUrl(port) {
486
+ return `http://${LOCAL_HOST}:${port}`;
487
+ }
488
+ function nodeEndpoint(baseUrl, path) {
489
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
490
+ return new URL(path, base).toString();
491
+ }
492
+ async function probeNodeHealth(baseUrl, options = {}) {
493
+ const { timeoutMs = DEFAULT_PROBE_TIMEOUT_MS, signal } = options;
494
+ if (signal?.aborted) return false;
495
+ const controller = new AbortController();
496
+ const onAbort = () => controller.abort();
497
+ const listenerAdded = !!signal;
498
+ if (listenerAdded) signal.addEventListener("abort", onAbort, { once: true });
499
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
500
+ try {
501
+ const url = nodeEndpoint(baseUrl, "admin-api/health");
502
+ const res = await fetch(url, {
503
+ method: "GET",
504
+ signal: controller.signal
505
+ });
506
+ if (!res.ok) return false;
507
+ try {
508
+ const body = await res.json();
509
+ const status = body?.data?.status !== void 0 ? body?.data?.status : body?.status;
510
+ if (status === void 0) return true;
511
+ return typeof status === "string" && status.toLowerCase() === "alive";
512
+ } catch {
513
+ return true;
514
+ }
515
+ } catch {
516
+ return false;
517
+ } finally {
518
+ clearTimeout(timer);
519
+ if (listenerAdded) signal.removeEventListener("abort", onAbort);
520
+ }
521
+ }
522
+ async function discoverLocalNodes(options = {}) {
523
+ const { ports = DEFAULT_LOCAL_NODE_PORTS, timeoutMs, signal } = options;
524
+ const results = await Promise.all(
525
+ ports.map(async (port) => {
526
+ const url = localNodeUrl(port);
527
+ const ok = await probeNodeHealth(url, { timeoutMs, signal });
528
+ return ok ? url : null;
529
+ })
530
+ );
531
+ return results.filter((url) => url !== null);
532
+ }
533
+
476
534
  // src/theme.ts
477
535
  var defaultMeroTheme = Object.freeze({
478
536
  primary: "#a5ff11",
@@ -521,6 +579,8 @@ function themeToCssVars(theme) {
521
579
  function cssVar(theme, key) {
522
580
  return `var(${MERO_CSS_VARS[key]}, ${theme[key]})`;
523
581
  }
582
+ var DEFAULT_LOCAL_NODE_URL = "http://node1.127.0.0.1.nip.io";
583
+ var CUSTOM_SELECTION = "__custom__";
524
584
  function isValidUrl(urlString) {
525
585
  if (!urlString || urlString.trim() === "") {
526
586
  return false;
@@ -549,6 +609,9 @@ function isValidUrl(urlString) {
549
609
  return false;
550
610
  }
551
611
  }
612
+ function displayNodeUrl(url) {
613
+ return url.replace(/^https?:\/\//, "").replace(/\/+$/, "");
614
+ }
552
615
  function tint(color, percent) {
553
616
  return `color-mix(in srgb, ${color} ${percent}%, transparent)`;
554
617
  }
@@ -653,10 +716,60 @@ function buildStyles(t) {
653
716
  transition: "all 0.15s ease"
654
717
  },
655
718
  radioLabelActive: {
656
- borderColor: accent,
719
+ border: `1px solid ${accent}`,
657
720
  backgroundColor: accentGlow,
658
721
  color: text
659
722
  },
723
+ radioList: {
724
+ display: "flex",
725
+ flexDirection: "column",
726
+ gap: "0.5rem",
727
+ marginBottom: "1rem"
728
+ },
729
+ radioItem: {
730
+ display: "flex",
731
+ alignItems: "center",
732
+ gap: "0.625rem",
733
+ color: text,
734
+ cursor: "pointer",
735
+ padding: "0.75rem 1rem",
736
+ borderRadius: radius,
737
+ border: `1px solid ${border}`,
738
+ backgroundColor: bgSecondary,
739
+ transition: "all 0.15s ease",
740
+ fontSize: "0.875rem"
741
+ },
742
+ radioItemActive: {
743
+ // Override the full `border` shorthand (not just borderColor) so React
744
+ // never has to mix shorthand + longhand on the same element.
745
+ border: `1px solid ${accent}`,
746
+ backgroundColor: accentGlow,
747
+ color: text
748
+ },
749
+ radioIndicator: {
750
+ flexShrink: 0,
751
+ width: "1rem",
752
+ height: "1rem",
753
+ borderRadius: "50%",
754
+ border: `2px solid ${border}`,
755
+ display: "flex",
756
+ alignItems: "center",
757
+ justifyContent: "center"
758
+ },
759
+ radioIndicatorActive: {
760
+ border: `2px solid ${accent}`
761
+ },
762
+ radioDot: {
763
+ width: "0.5rem",
764
+ height: "0.5rem",
765
+ borderRadius: "50%",
766
+ backgroundColor: accent
767
+ },
768
+ nodeMeta: {
769
+ marginLeft: "auto",
770
+ fontSize: "0.75rem",
771
+ color: textSecondary
772
+ },
660
773
  input: {
661
774
  width: "100%",
662
775
  padding: "0.75rem 1rem",
@@ -682,6 +795,29 @@ function buildStyles(t) {
682
795
  localInfoCode: {
683
796
  color: accent
684
797
  },
798
+ noNodeInfo: {
799
+ color: textSecondary,
800
+ fontSize: "0.875rem",
801
+ textAlign: "center",
802
+ padding: "0.75rem",
803
+ backgroundColor: bgSecondary,
804
+ borderRadius: radius,
805
+ marginBottom: "1rem",
806
+ border: `1px solid ${border}`
807
+ },
808
+ toolbar: {
809
+ display: "flex",
810
+ justifyContent: "center",
811
+ marginBottom: "1rem"
812
+ },
813
+ rescan: {
814
+ background: "none",
815
+ border: "none",
816
+ color: accent,
817
+ cursor: "pointer",
818
+ fontSize: "0.8125rem",
819
+ padding: "0.25rem 0.5rem"
820
+ },
685
821
  buttonGroup: {
686
822
  display: "flex",
687
823
  justifyContent: "center"
@@ -709,6 +845,15 @@ function buildStyles(t) {
709
845
  padding: "2rem",
710
846
  color: textSecondary
711
847
  },
848
+ discovering: {
849
+ display: "flex",
850
+ flexDirection: "column",
851
+ alignItems: "center",
852
+ gap: "0.75rem",
853
+ padding: "1rem",
854
+ color: textSecondary,
855
+ fontSize: "0.875rem"
856
+ },
712
857
  spinner: {
713
858
  width: "2rem",
714
859
  height: "2rem",
@@ -716,6 +861,14 @@ function buildStyles(t) {
716
861
  borderTopColor: accent,
717
862
  borderRadius: "50%",
718
863
  animation: "meroSpin 1s linear infinite"
864
+ },
865
+ spinnerSmall: {
866
+ width: "1.5rem",
867
+ height: "1.5rem",
868
+ border: `3px solid ${border}`,
869
+ borderTopColor: accent,
870
+ borderRadius: "50%",
871
+ animation: "meroSpin 1s linear infinite"
719
872
  }
720
873
  };
721
874
  }
@@ -724,11 +877,14 @@ function LoginModal({
724
877
  onClose,
725
878
  connectionType,
726
879
  isOpen,
727
- theme
880
+ theme,
881
+ localNodePorts = DEFAULT_LOCAL_NODE_PORTS
728
882
  }) {
729
883
  const [nodeType, setNodeType] = react.useState("local");
730
- const [nodeUrl, setNodeUrl2] = react.useState("");
731
- const [isValid, setIsValid] = react.useState(true);
884
+ const [selected, setSelected] = react.useState(CUSTOM_SELECTION);
885
+ const [discovered, setDiscovered] = react.useState([]);
886
+ const [discovering, setDiscovering] = react.useState(false);
887
+ const [customUrl, setCustomUrl] = react.useState("");
732
888
  const [loading, setLoading] = react.useState(false);
733
889
  const [error, setError] = react.useState(null);
734
890
  const resolved = react.useMemo(() => resolveMeroTheme(theme), [theme]);
@@ -743,7 +899,7 @@ function LoginModal({
743
899
  react.useEffect(() => {
744
900
  const savedUrl = localStorage.getItem("mero:node_url");
745
901
  if (savedUrl) {
746
- setNodeUrl2(savedUrl);
902
+ setCustomUrl(savedUrl);
747
903
  }
748
904
  }, []);
749
905
  react.useEffect(() => {
@@ -753,25 +909,56 @@ function LoginModal({
753
909
  setNodeType("remote");
754
910
  }
755
911
  }, [connectionType]);
912
+ const [scanNonce, setScanNonce] = react.useState(0);
913
+ const portsKey = react.useMemo(() => localNodePorts.join(","), [localNodePorts]);
914
+ const remoteActive = isOpen && shouldShowRemote && nodeType === "remote";
756
915
  react.useEffect(() => {
757
- if (nodeType === "remote") {
758
- setIsValid(isValidUrl(nodeUrl));
759
- } else {
760
- setIsValid(true);
916
+ if (!remoteActive) {
917
+ return;
761
918
  }
762
- }, [nodeUrl, nodeType]);
919
+ const controller = new AbortController();
920
+ let active = true;
921
+ setDiscovering(true);
922
+ setDiscovered([]);
923
+ setError(null);
924
+ discoverLocalNodes({ ports: localNodePorts, signal: controller.signal }).then((nodes) => {
925
+ if (!active) return;
926
+ setDiscovered(nodes);
927
+ setSelected(nodes.length > 0 ? nodes[0] : CUSTOM_SELECTION);
928
+ }).catch(() => {
929
+ if (active) setSelected(CUSTOM_SELECTION);
930
+ }).finally(() => {
931
+ if (active) setDiscovering(false);
932
+ });
933
+ return () => {
934
+ active = false;
935
+ controller.abort();
936
+ };
937
+ }, [remoteActive, portsKey, scanNonce]);
938
+ const isCustom = selected === CUSTOM_SELECTION;
939
+ const hasDiscovered = discovered.length > 0;
940
+ const canConnect = !loading && (nodeType === "local" ? true : discovering ? false : isCustom ? isValidUrl(customUrl) : true);
941
+ const canConnectRef = react.useRef(canConnect);
942
+ canConnectRef.current = canConnect;
763
943
  const handleConnect = react.useCallback(async () => {
764
- if (!isValid) return;
944
+ const targetUrl = nodeType === "local" ? DEFAULT_LOCAL_NODE_URL : selected === CUSTOM_SELECTION ? customUrl : selected;
945
+ const usingDiscovered = nodeType === "remote" && selected !== CUSTOM_SELECTION;
946
+ if (nodeType === "remote" && !usingDiscovered && !isValidUrl(targetUrl)) {
947
+ return;
948
+ }
949
+ const normalizedUrl = targetUrl.replace(/\/+$/, "");
950
+ if (usingDiscovered) {
951
+ onConnect(normalizedUrl);
952
+ return;
953
+ }
765
954
  setLoading(true);
766
955
  setError(null);
767
- const baseUrl = nodeType === "local" ? "http://node1.127.0.0.1.nip.io" : nodeUrl;
768
956
  try {
769
957
  const response = await fetch(
770
- new URL("admin-api/is-authed", baseUrl).toString()
958
+ nodeEndpoint(normalizedUrl, "admin-api/is-authed")
771
959
  );
772
960
  if (response.ok || response.status === 401) {
773
961
  setLoading(false);
774
- const normalizedUrl = baseUrl.replace(/\/+$/, "");
775
962
  onConnect(normalizedUrl);
776
963
  } else {
777
964
  throw new Error(`Connection failed: ${response.statusText}`);
@@ -781,10 +968,108 @@ function LoginModal({
781
968
  setError("Failed to connect. Please check the URL and try again.");
782
969
  setLoading(false);
783
970
  }
784
- }, [isValid, nodeType, nodeUrl, onConnect]);
971
+ }, [nodeType, selected, customUrl, onConnect]);
785
972
  if (!isOpen) {
786
973
  return null;
787
974
  }
975
+ const showManualInput = isCustom;
976
+ const renderRadio = (value, label, meta) => {
977
+ const active = selected === value;
978
+ return (
979
+ // Selection is driven solely by the radio input's `onChange` — clicking
980
+ // anywhere on the wrapping label forwards to the input, and keyboard
981
+ // users can tab to / arrow through the (visually hidden but focusable)
982
+ // input. A label `onClick` here would double-fire `setSelected`.
983
+ /* @__PURE__ */ jsxRuntime.jsxs(
984
+ "label",
985
+ {
986
+ "data-testid": `node-option-${value === CUSTOM_SELECTION ? "custom" : displayNodeUrl(value)}`,
987
+ style: {
988
+ ...styles.radioItem,
989
+ ...active ? styles.radioItemActive : {}
990
+ },
991
+ children: [
992
+ /* @__PURE__ */ jsxRuntime.jsx(
993
+ "input",
994
+ {
995
+ type: "radio",
996
+ name: "mero-node",
997
+ value,
998
+ checked: active,
999
+ onChange: () => setSelected(value),
1000
+ style: { position: "absolute", opacity: 0 }
1001
+ }
1002
+ ),
1003
+ /* @__PURE__ */ jsxRuntime.jsx(
1004
+ "span",
1005
+ {
1006
+ style: {
1007
+ ...styles.radioIndicator,
1008
+ ...active ? styles.radioIndicatorActive : {}
1009
+ },
1010
+ children: active && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.radioDot })
1011
+ }
1012
+ ),
1013
+ label,
1014
+ meta && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.nodeMeta, children: meta })
1015
+ ]
1016
+ },
1017
+ value
1018
+ )
1019
+ );
1020
+ };
1021
+ const renderRemoteView = () => {
1022
+ if (discovering) {
1023
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.discovering, "data-testid": "node-discovering", children: [
1024
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.spinnerSmall }),
1025
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Searching for local nodes..." })
1026
+ ] });
1027
+ }
1028
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1029
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.info, children: hasDiscovered ? "Select a discovered node, or enter a node URL manually." : "No local node found. Enter a node URL to continue." }),
1030
+ hasDiscovered && /* @__PURE__ */ jsxRuntime.jsxs(
1031
+ "div",
1032
+ {
1033
+ style: styles.radioList,
1034
+ role: "radiogroup",
1035
+ "aria-label": "Available nodes",
1036
+ children: [
1037
+ discovered.map(
1038
+ (url) => renderRadio(url, displayNodeUrl(url), "local")
1039
+ ),
1040
+ renderRadio(CUSTOM_SELECTION, "Enter node URL manually")
1041
+ ]
1042
+ }
1043
+ ),
1044
+ showManualInput && /* @__PURE__ */ jsxRuntime.jsx(
1045
+ "input",
1046
+ {
1047
+ type: "text",
1048
+ value: customUrl,
1049
+ onChange: (e) => setCustomUrl(e.target.value),
1050
+ placeholder: "https://your-node-url.calimero.network",
1051
+ style: styles.input,
1052
+ "data-testid": "node-url-input",
1053
+ autoFocus: !hasDiscovered,
1054
+ onKeyDown: (e) => {
1055
+ if (e.key === "Enter" && canConnectRef.current) {
1056
+ handleConnect();
1057
+ }
1058
+ }
1059
+ }
1060
+ ),
1061
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.toolbar, children: /* @__PURE__ */ jsxRuntime.jsx(
1062
+ "button",
1063
+ {
1064
+ type: "button",
1065
+ style: styles.rescan,
1066
+ onClick: () => setScanNonce((n) => n + 1),
1067
+ "data-testid": "rescan-button",
1068
+ children: "\u21BB Rescan local nodes"
1069
+ }
1070
+ ) })
1071
+ ] });
1072
+ };
788
1073
  const modalContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
789
1074
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
790
1075
  @keyframes meroSpin { to { transform: rotate(360deg); } }
@@ -792,7 +1077,7 @@ function LoginModal({
792
1077
  @keyframes meroSlideIn { from { transform: translateY(-12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
793
1078
  ` }),
794
1079
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...themeVars, ...styles.overlay }, onClick: onClose, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.content, onClick: (e) => e.stopPropagation(), children: [
795
- /* @__PURE__ */ jsxRuntime.jsx("button", { style: styles.closeButton, onClick: onClose, children: "\xD7" }),
1080
+ /* @__PURE__ */ jsxRuntime.jsx("button", { style: styles.closeButton, onClick: onClose, "aria-label": "Close", children: "\xD7" }),
796
1081
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.header, children: [
797
1082
  /* @__PURE__ */ jsxRuntime.jsx(CalimeroLogo, { size: 44, color: cssVar(resolved, "primary") }),
798
1083
  /* @__PURE__ */ jsxRuntime.jsx("h1", { style: styles.title, children: "Connect to Calimero" })
@@ -801,12 +1086,13 @@ function LoginModal({
801
1086
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Connecting to node..." }),
802
1087
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.spinner })
803
1088
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
804
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.info, children: shouldShowRadioGroup ? "Select your Calimero node type to continue." : connectionType === "local" /* Local */ ? "Connect to your local Calimero node." : "Enter your remote Calimero node URL." }),
1089
+ shouldShowRadioGroup && /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.info, children: "Select your Calimero node type to continue." }),
805
1090
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.error, children: error }),
806
1091
  shouldShowRadioGroup && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.radioGroup, children: [
807
1092
  /* @__PURE__ */ jsxRuntime.jsxs(
808
1093
  "label",
809
1094
  {
1095
+ "data-testid": "node-type-local",
810
1096
  style: {
811
1097
  ...styles.radioLabel,
812
1098
  ...nodeType === "local" ? styles.radioLabelActive : {}
@@ -830,6 +1116,7 @@ function LoginModal({
830
1116
  /* @__PURE__ */ jsxRuntime.jsxs(
831
1117
  "label",
832
1118
  {
1119
+ "data-testid": "node-type-remote",
833
1120
  style: {
834
1121
  ...styles.radioLabel,
835
1122
  ...nodeType === "remote" ? styles.radioLabelActive : {}
@@ -851,34 +1138,22 @@ function LoginModal({
851
1138
  }
852
1139
  )
853
1140
  ] }),
854
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: shouldShowRemote && nodeType === "remote" ? /* @__PURE__ */ jsxRuntime.jsx(
855
- "input",
856
- {
857
- type: "text",
858
- value: nodeUrl,
859
- onChange: (e) => setNodeUrl2(e.target.value),
860
- placeholder: "https://your-node-url.calimero.network",
861
- style: styles.input,
862
- onKeyDown: (e) => {
863
- if (e.key === "Enter" && isValid) {
864
- handleConnect();
865
- }
866
- }
867
- }
868
- ) : shouldShowLocal ? /* @__PURE__ */ jsxRuntime.jsxs("p", { style: styles.localInfo, children: [
1141
+ nodeType === "local" && shouldShowLocal && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: styles.localInfo, children: [
869
1142
  "Using default local node: ",
870
1143
  /* @__PURE__ */ jsxRuntime.jsx("br", {}),
871
- /* @__PURE__ */ jsxRuntime.jsx("code", { style: styles.localInfoCode, children: "http://node1.127.0.0.1.nip.io" })
872
- ] }) : null }),
1144
+ /* @__PURE__ */ jsxRuntime.jsx("code", { style: styles.localInfoCode, children: DEFAULT_LOCAL_NODE_URL })
1145
+ ] }),
1146
+ nodeType === "remote" && shouldShowRemote && renderRemoteView(),
873
1147
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.buttonGroup, children: /* @__PURE__ */ jsxRuntime.jsx(
874
1148
  "button",
875
1149
  {
876
1150
  onClick: handleConnect,
877
- disabled: !isValid || loading,
1151
+ disabled: !canConnect,
878
1152
  style: {
879
1153
  ...styles.button,
880
- ...!isValid || loading ? styles.buttonDisabled : {}
1154
+ ...!canConnect ? styles.buttonDisabled : {}
881
1155
  },
1156
+ "data-testid": "connect-button",
882
1157
  children: "Connect"
883
1158
  }
884
1159
  ) })
@@ -2670,6 +2945,7 @@ exports.AppMode = AppMode;
2670
2945
  exports.CalimeroLogo = CalimeroLogo;
2671
2946
  exports.ConnectButton = ConnectButton;
2672
2947
  exports.ConnectionType = ConnectionType;
2948
+ exports.DEFAULT_LOCAL_NODE_PORTS = DEFAULT_LOCAL_NODE_PORTS;
2673
2949
  exports.LoginModal = LoginModal;
2674
2950
  exports.MERO_CSS_VARS = MERO_CSS_VARS;
2675
2951
  exports.MeroContext = MeroContext;
@@ -2683,11 +2959,15 @@ exports.clearContextIdentity = clearContextIdentity;
2683
2959
  exports.clearNodeUrl = clearNodeUrl;
2684
2960
  exports.cssVar = cssVar;
2685
2961
  exports.defaultMeroTheme = defaultMeroTheme;
2962
+ exports.discoverLocalNodes = discoverLocalNodes;
2686
2963
  exports.getApplicationId = getApplicationId;
2687
2964
  exports.getContextId = getContextId;
2688
2965
  exports.getContextIdentity = getContextIdentity;
2689
2966
  exports.getNodeUrl = getNodeUrl;
2967
+ exports.localNodeUrl = localNodeUrl;
2690
2968
  exports.localStorageTokenStorage = localStorageTokenStorage;
2969
+ exports.nodeEndpoint = nodeEndpoint;
2970
+ exports.probeNodeHealth = probeNodeHealth;
2691
2971
  exports.resolveMeroTheme = resolveMeroTheme;
2692
2972
  exports.setApplicationId = setApplicationId;
2693
2973
  exports.setContextId = setContextId;