@saptools/cf-hana 0.3.2 → 0.3.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ <!-- cspell:words VARCHAR -->
4
+
5
+ ## 0.3.5 - 2026-07-06
6
+
7
+ - Normalize HANA `BOOLEAN` result columns that arrive from the driver as `1`/`0`
8
+ into JavaScript `true`/`false` values before CLI/API formatting and saved refs.
9
+
10
+ ## 0.3.4 - 2026-07-02
11
+
12
+ - Keep explicit selector region lookup map-backed so unknown technical keys fail before isolated authentication unless they are added to the maintained SAP CF region catalog.
13
+
14
+ ## 0.3.3 - 2026-07-02
15
+
16
+ - Fix Cloud Foundry region resolution for indexed SAP regions such as `eu10-005`, representative `eu20`/`us10` indexed endpoints, and China `platform.sapcloud.cn` endpoints.
17
+ - Keep bare CLI selectors on the core current-session path so healthy `cf env <app>` calls do not require isolated SAP re-authentication.
18
+ - Preserve and validate the current CF API endpoint for auth fallback, reject unsafe endpoint shapes, and keep binding discovery live-only without restoring `cf-sync` cache reads.
19
+ - Update `--refresh`, README, and cf-hana skill text to describe the retained compatibility flag truthfully.
20
+
3
21
  ## 0.3.2 - 2026-07-01
4
22
 
5
23
  - Remove the `cf-hana: saved result expires at...` stderr notice from `query --save` output while keeping result refs available for inspection.
package/README.md CHANGED
@@ -2,19 +2,17 @@
2
2
 
3
3
  > Run SQL directly against SAP HANA Cloud databases bound to a Cloud Foundry app — addressed by a `region/org/space/app` selector.
4
4
 
5
- `@saptools/cf-hana` closes the last gap in the `saptools` chain: `@saptools/cf-sync`
6
- already discovers Cloud Foundry topology and HANA service bindings, but nothing
7
- actually executes SQL. This package does. Pass a selector, get a connected,
8
- pooled client, and run `SELECT` / `INSERT` / `UPDATE` / `DELETE` / DDL.
9
-
10
- It is fast because credentials come from `cf-sync`'s on-disk cache (no 5–15s CF
11
- login on the hot path) and connections are pooled and reused within a process.
5
+ `@saptools/cf-hana` opens live SAP HANA Cloud connections from Cloud Foundry app
6
+ bindings. Pass a selector, get a connected, pooled client, and run `SELECT` /
7
+ `INSERT` / `UPDATE` / `DELETE` / DDL. Bare app names use your active `cf target`
8
+ and current CF session first, so a healthy local CF login does not require SAP
9
+ password re-authentication.
12
10
 
13
11
  ## Features
14
12
 
15
13
  - **Selector-based connect** — `region/org/space/app` or a bare app name.
16
- - **Credentials, handled for you** — cache-first via `@saptools/cf-sync`, with an
17
- on-demand live Cloud Foundry fetch as a fallback.
14
+ - **Credentials, handled for you** — HANA bindings are read live from Cloud Foundry.
15
+ Bare app names use the current CF session first; explicit selectors use isolated live authentication.
18
16
  - **Parameterized queries** — values always travel as bound `?` parameters, never
19
17
  string-concatenated.
20
18
  - **Connection pooling** — pooled, reused connections; opt out with `pool: false`.
@@ -74,8 +72,9 @@ Every entry point takes a selector as its first argument:
74
72
 
75
73
  - **Explicit** — `region/org/space/app` (e.g.
76
74
  `eu10/example-org/space-demo/app-demo`). Works without any cached topology.
77
- - **Bare app name** — `app-demo`. Resolved against the topology cached by
78
- `cf-sync sync`; throws if the name is ambiguous across spaces.
75
+ - **Bare app name** — `app-demo`. Resolved only against the active `cf target`
76
+ (current org/space/API endpoint) and fetched with `cf env app-demo` using your
77
+ existing CF session before any isolated re-auth fallback.
79
78
 
80
79
  ## CLI
81
80
 
@@ -89,7 +88,7 @@ cf-hana info <selector> Print the resolved connection metada
89
88
  cf-hana result <command> Inspect saved query refs
90
89
  ```
91
90
 
92
- Common options: `--refresh`, `--role <runtime|hdi>`, `--binding <name>` /
91
+ Common options: `--refresh` (deprecated compatibility flag; binding discovery is already live), `--role <runtime|hdi>`, `--binding <name>` /
93
92
  `--binding-index <n>`, `--timeout <ms>`, `--read-only`, `--allow-destructive`,
94
93
  `--limit <n>`, `--no-auto-limit`. The `query` command also accepts
95
94
  `--param <value>` (repeatable), `--cell-limit <n>`, `--save`,
@@ -170,10 +169,11 @@ atomically, and expire after exactly 30 minutes. The cache key is derived from
170
169
  non-secret connection identity: selector, app name, host, active schema, role,
171
170
  and driver. It does not include passwords, certificates, tokens, SQL parameter
172
171
  values, result rows, or table data, and malformed cache files are treated as
173
- misses. Pass `--refresh-metadata` to bypass this cache for a query; `--refresh`
174
- also bypasses it because it refreshes the resolved connection identity. If
175
- metadata lookup or cache I/O fails, cf-hana preserves the original query error
176
- and simply omits suggestions.
172
+ misses. Pass `--refresh-metadata` to bypass this cache for a query. The legacy
173
+ `--refresh` flag is accepted for compatibility but does not bypass the metadata
174
+ cache and is not a credential-cache control because binding discovery is already
175
+ live. If metadata lookup or cache I/O fails, cf-hana preserves the original
176
+ query error and simply omits suggestions.
177
177
 
178
178
  ## Programmatic API
179
179
 
@@ -187,21 +187,31 @@ and simply omits suggestions.
187
187
 
188
188
  `ConnectOptions` highlights: `role` (`runtime` | `hdi`), `bindingName` /
189
189
  `bindingIndex`, `readOnly`, `allowDestructive`, `autoLimit`, `queryTimeoutMs`,
190
- `connectTimeoutMs`, `refresh`, `pool`.
190
+ `connectTimeoutMs`, deprecated `refresh`, `pool`.
191
191
 
192
192
  ## Credentials
193
193
 
194
- Credentials are resolved **cache-first**:
195
-
196
- 1. Read what `cf-sync db-sync` cached in `~/.saptools/cf-db-bindings.json`.
197
- 2. On a cache miss (or when `refresh: true` / `--refresh` is passed), fetch them
198
- live from Cloud Foundry. The live fetch needs `SAP_EMAIL` and `SAP_PASSWORD`
199
- (or the `email` / `password` options) and never persists anything to disk.
200
-
201
- Credential resolution writes nothing under `~/.saptools/` it only reads what
202
- `cf-sync` cached. The connection pool is in-process and in-memory only, so it is
203
- safe to run many `cf-hana` processes in parallel and alongside any `cf-sync`
204
- command.
194
+ Credential discovery is **live-only** and does not read `@saptools/cf-sync`
195
+ snapshots or `~/.saptools/cf-db-bindings.json`.
196
+
197
+ - Bare app selectors (`app-demo`) read the active `cf target`, preserve its exact
198
+ validated API endpoint, and first run `cf env app-demo` with your current CF
199
+ session. This path does not require `SAP_EMAIL` or `SAP_PASSWORD` while the
200
+ current session is healthy.
201
+ - If that direct bare-app call fails with an auth/session error, cf-hana falls
202
+ back to an isolated temporary `CF_HOME`, runs `cf api <current-endpoint>`,
203
+ authenticates with `SAP_EMAIL` / `SAP_PASSWORD` (or the programmatic `email` /
204
+ `password` options), targets the current org/space, and reads `cf env`.
205
+ - Explicit `region/org/space/app` selectors always use isolated live
206
+ authentication. Region keys support current SAP CF technical keys, including
207
+ indexed regions such as `eu10-005`, `eu20-001`, `us10-002`, and China
208
+ endpoints such as `cn40` on `platform.sapcloud.cn`.
209
+
210
+ The legacy `refresh` / `--refresh` option is retained only for compatibility and
211
+ has no credential-cache meaning; binding discovery is already live. Use
212
+ `--refresh-metadata` when you specifically want to bypass the table/view
213
+ suggestion metadata cache. Credential resolution writes no binding credentials
214
+ under `~/.saptools/`.
205
215
 
206
216
  ## SQL history
207
217
 
@@ -264,9 +274,9 @@ parameters.
264
274
  ## Requirements
265
275
 
266
276
  - Node.js >= 20.
267
- - A HANA binding reachable from your network. Resolving a bare app name, or a
268
- live credential fetch, additionally needs the Cloud Foundry CLI and
269
- `SAP_EMAIL` / `SAP_PASSWORD`.
277
+ - A HANA binding reachable from your network. Resolving a bare app name needs the Cloud Foundry CLI and an active `cf target`.
278
+ Isolated fallback or explicit selectors additionally need `SAP_EMAIL` /
279
+ `SAP_PASSWORD`.
270
280
 
271
281
  ## Development
272
282
 
package/dist/cli.js CHANGED
@@ -698,10 +698,10 @@ async function listColumns(connection, schema, table) {
698
698
 
699
699
  // src/config.ts
700
700
  var CLI_NAME = "cf-hana";
701
- var CLI_VERSION = "0.3.2";
701
+ var CLI_VERSION = "0.3.5";
702
702
  var ENV_PREFIX = "CF_HANA";
703
- var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
704
- var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
703
+ var DEFAULT_QUERY_TIMEOUT_MS = 6e4;
704
+ var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
705
705
  var DEFAULT_POOL_MAX = 4;
706
706
  var DEFAULT_POOL_IDLE_MS = 6e4;
707
707
  var DEFAULT_AUTO_LIMIT = 100;
@@ -738,9 +738,9 @@ import { join as join2 } from "path";
738
738
  import { promisify } from "util";
739
739
  var execFileAsync = promisify(execFile);
740
740
  var MAX_BUFFER = 16 * 1024 * 1024;
741
- var DEFAULT_TIMEOUT_MS = 3e4;
741
+ var DEFAULT_TIMEOUT_MS = 6e4;
742
742
  var CF_RETRY_ATTEMPTS = 3;
743
- var CF_RETRY_BASE_DELAY_MS = 120;
743
+ var CF_RETRY_BASE_DELAY_MS = 500;
744
744
  var REGION_API_MAP = {
745
745
  ae01: "https://api.cf.ae01.hana.ondemand.com",
746
746
  ap01: "https://api.cf.ap01.hana.ondemand.com",
@@ -750,40 +750,94 @@ var REGION_API_MAP = {
750
750
  ap20: "https://api.cf.ap20.hana.ondemand.com",
751
751
  ap21: "https://api.cf.ap21.hana.ondemand.com",
752
752
  ap30: "https://api.cf.ap30.hana.ondemand.com",
753
+ ap31: "https://api.cf.ap31.hana.ondemand.com",
753
754
  br10: "https://api.cf.br10.hana.ondemand.com",
754
755
  br20: "https://api.cf.br20.hana.ondemand.com",
755
756
  br30: "https://api.cf.br30.hana.ondemand.com",
756
757
  ca10: "https://api.cf.ca10.hana.ondemand.com",
757
758
  ca20: "https://api.cf.ca20.hana.ondemand.com",
758
759
  ch20: "https://api.cf.ch20.hana.ondemand.com",
760
+ cn20: "https://api.cf.cn20.platform.sapcloud.cn",
761
+ cn40: "https://api.cf.cn40.platform.sapcloud.cn",
762
+ eu01: "https://api.cf.eu01.hana.ondemand.com",
763
+ eu02: "https://api.cf.eu02.hana.ondemand.com",
759
764
  eu10: "https://api.cf.eu10.hana.ondemand.com",
765
+ "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
766
+ "eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
767
+ "eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
768
+ "eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
769
+ "eu10-006": "https://api.cf.eu10-006.hana.ondemand.com",
760
770
  eu11: "https://api.cf.eu11.hana.ondemand.com",
761
771
  eu12: "https://api.cf.eu12.hana.ondemand.com",
772
+ eu13: "https://api.cf.eu13.hana.ondemand.com",
762
773
  eu20: "https://api.cf.eu20.hana.ondemand.com",
774
+ "eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
775
+ "eu20-002": "https://api.cf.eu20-002.hana.ondemand.com",
763
776
  eu21: "https://api.cf.eu21.hana.ondemand.com",
777
+ eu22: "https://api.cf.eu22.hana.ondemand.com",
764
778
  eu30: "https://api.cf.eu30.hana.ondemand.com",
765
- "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
779
+ eu31: "https://api.cf.eu31.hana.ondemand.com",
780
+ il30: "https://api.cf.il30.hana.ondemand.com",
781
+ in30: "https://api.cf.in30.hana.ondemand.com",
782
+ jp01: "https://api.cf.jp01.hana.ondemand.com",
766
783
  jp10: "https://api.cf.jp10.hana.ondemand.com",
767
784
  jp20: "https://api.cf.jp20.hana.ondemand.com",
768
785
  jp30: "https://api.cf.jp30.hana.ondemand.com",
786
+ jp31: "https://api.cf.jp31.hana.ondemand.com",
787
+ sa30: "https://api.cf.sa30.hana.ondemand.com",
788
+ sa31: "https://api.cf.sa31.hana.ondemand.com",
789
+ uk20: "https://api.cf.uk20.hana.ondemand.com",
790
+ us01: "https://api.cf.us01.hana.ondemand.com",
791
+ us02: "https://api.cf.us02.hana.ondemand.com",
769
792
  us10: "https://api.cf.us10.hana.ondemand.com",
793
+ "us10-001": "https://api.cf.us10-001.hana.ondemand.com",
794
+ "us10-002": "https://api.cf.us10-002.hana.ondemand.com",
795
+ "us10-003": "https://api.cf.us10-003.hana.ondemand.com",
770
796
  us11: "https://api.cf.us11.hana.ondemand.com",
771
797
  us20: "https://api.cf.us20.hana.ondemand.com",
772
798
  us21: "https://api.cf.us21.hana.ondemand.com",
799
+ "us21-001": "https://api.cf.us21-001.hana.ondemand.com",
800
+ us22: "https://api.cf.us22.hana.ondemand.com",
773
801
  us30: "https://api.cf.us30.hana.ondemand.com",
774
- in30: "https://api.cf.in30.hana.ondemand.com"
802
+ us32: "https://api.cf.us32.hana.ondemand.com"
775
803
  };
804
+ function normalizeSapCfApiEndpoint(apiEndpoint) {
805
+ const trimmed = apiEndpoint.trim();
806
+ let parsed;
807
+ try {
808
+ parsed = new URL(trimmed);
809
+ } catch {
810
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
811
+ }
812
+ if (/^https:\/\/[^/]*:\d+(?:[/?#]|$)/i.test(trimmed)) {
813
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
814
+ }
815
+ if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.pathname.replace(/\/+$/, "") !== "" || parsed.search !== "" || parsed.hash !== "") {
816
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
817
+ }
818
+ const hostname = parsed.hostname.toLowerCase();
819
+ const match = /^api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.(hana\.ondemand\.com|platform\.sapcloud\.cn)$/.exec(hostname);
820
+ if (!match) {
821
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
822
+ }
823
+ return `https://${hostname}`;
824
+ }
776
825
  function getApiEndpointForRegion(regionKey) {
777
- return REGION_API_MAP[regionKey];
826
+ return REGION_API_MAP[regionKey.trim().toLowerCase()];
778
827
  }
779
828
  function getRegionKeyForApi(apiEndpoint) {
780
- const norm2 = apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
829
+ let normalized;
830
+ try {
831
+ normalized = normalizeSapCfApiEndpoint(apiEndpoint);
832
+ } catch {
833
+ return void 0;
834
+ }
781
835
  for (const [key, endpoint] of Object.entries(REGION_API_MAP)) {
782
- if (endpoint.toLowerCase() === norm2) {
836
+ if (endpoint.toLowerCase() === normalized) {
783
837
  return key;
784
838
  }
785
839
  }
786
- return void 0;
840
+ return /^https:\/\/api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.hana\.ondemand\.com$/.exec(normalized)?.[1];
787
841
  }
788
842
  async function withCfSession(work) {
789
843
  const cfHome = await mkdtemp(join2(tmpdir(), "saptools-cf-hana-"));
@@ -808,29 +862,45 @@ function buildEnv(ctx, overrides = {}) {
808
862
  env["CF_HOME"] = ctx.cfHome;
809
863
  return env;
810
864
  }
811
- async function runCf(args, ctx, overrides = {}) {
812
- const { bin, argsPrefix } = resolveCfBin();
813
- const env = buildEnv(ctx, overrides);
865
+ async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS) {
814
866
  let lastErr;
815
867
  for (let attempt = 0; attempt < CF_RETRY_ATTEMPTS; attempt++) {
816
868
  try {
817
- const { stdout } = await execFileAsync(bin, [...argsPrefix, ...args], {
869
+ const { stdout } = await execFileAsync(bin, args, {
818
870
  env,
819
871
  maxBuffer: MAX_BUFFER,
820
- timeout: DEFAULT_TIMEOUT_MS
872
+ timeout: timeoutMs,
873
+ killSignal: "SIGKILL"
821
874
  });
822
875
  return stdout;
823
876
  } catch (err) {
824
877
  lastErr = err;
878
+ const e = err;
879
+ const output = `${e.message ?? ""} ${e.stderr ? String(e.stderr) : ""}`.toLowerCase();
880
+ const isTimeout = e.killed === true;
881
+ const isNetworkFlake = output.includes("error performing request") || output.includes("timeout") || output.includes("connection reset") || output.includes("connection refused") || output.includes("eof") || output.includes("502 bad gateway") || output.includes("503 service unavailable") || output.includes("504 gateway timeout") || output.includes("dial tcp") || output.includes("no such host");
882
+ const isEnoent = e.code === "ENOENT";
883
+ if (isEnoent || !isTimeout && !isNetworkFlake) {
884
+ throw err;
885
+ }
825
886
  if (attempt < CF_RETRY_ATTEMPTS - 1) {
826
887
  await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
827
888
  }
828
889
  }
829
890
  }
830
- const e = lastErr;
831
- const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
832
- const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
833
- throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
891
+ throw lastErr;
892
+ }
893
+ async function runCf(args, ctx, overrides = {}) {
894
+ const { bin, argsPrefix } = resolveCfBin();
895
+ const env = buildEnv(ctx, overrides);
896
+ try {
897
+ return await execWithRetries(bin, [...argsPrefix, ...args], env);
898
+ } catch (lastErr) {
899
+ const e = lastErr;
900
+ const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
901
+ const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
902
+ throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
903
+ }
834
904
  }
835
905
  async function cfApi(apiEndpoint, ctx) {
836
906
  await runCf(["api", apiEndpoint], ctx);
@@ -852,12 +922,7 @@ async function cfEnvDirect(appName) {
852
922
  const env = { ...process.env };
853
923
  delete env["SAP_EMAIL"];
854
924
  delete env["SAP_PASSWORD"];
855
- const { stdout } = await execFileAsync(bin, [...argsPrefix, "env", appName], {
856
- env,
857
- maxBuffer: MAX_BUFFER,
858
- timeout: DEFAULT_TIMEOUT_MS
859
- });
860
- return stdout;
925
+ return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
861
926
  }
862
927
  async function readCurrentCfTarget() {
863
928
  const { bin, argsPrefix } = resolveCfBin();
@@ -865,11 +930,7 @@ async function readCurrentCfTarget() {
865
930
  delete env["SAP_EMAIL"];
866
931
  delete env["SAP_PASSWORD"];
867
932
  try {
868
- const { stdout } = await execFileAsync(bin, [...argsPrefix, "target"], {
869
- env,
870
- maxBuffer: MAX_BUFFER,
871
- timeout: 1e4
872
- });
933
+ const stdout = await execWithRetries(bin, [...argsPrefix, "target"], env);
873
934
  return parseCfTargetOutput(stdout);
874
935
  } catch {
875
936
  return void 0;
@@ -883,9 +944,15 @@ function parseCfTargetOutput(stdout) {
883
944
  if (!api || !org || !space) {
884
945
  return void 0;
885
946
  }
886
- const regionKey = getRegionKeyForApi(api);
947
+ let normalizedApi;
948
+ try {
949
+ normalizedApi = normalizeSapCfApiEndpoint(api);
950
+ } catch {
951
+ return void 0;
952
+ }
953
+ const regionKey = getRegionKeyForApi(normalizedApi);
887
954
  return {
888
- apiEndpoint: api,
955
+ apiEndpoint: normalizedApi,
889
956
  orgName: org,
890
957
  spaceName: space,
891
958
  ...regionKey ? { regionKey } : {}
@@ -911,12 +978,8 @@ function formatCurrentCfAppSelector(target, appName) {
911
978
  if (!name) {
912
979
  throw new Error("App name is required.");
913
980
  }
914
- if (!target.regionKey) {
915
- throw new Error(
916
- `Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. Pass a full region/org/space/app selector.`
917
- );
918
- }
919
- return `${target.regionKey}/${target.orgName}/${target.spaceName}/${name}`;
981
+ const region = target.regionKey ?? "current";
982
+ return `${region}/${target.orgName}/${target.spaceName}/${name}`;
920
983
  }
921
984
  function extractVcapSection(stdout) {
922
985
  const start = stdout.indexOf("VCAP_SERVICES:");
@@ -996,6 +1059,9 @@ function classifyCfError(stderr = "", stdout = "") {
996
1059
  }
997
1060
 
998
1061
  // src/credentials.ts
1062
+ function errorMessageFromUnknown(error) {
1063
+ return error instanceof Error ? error.message : "Invalid or untrusted CF API endpoint.";
1064
+ }
999
1065
  async function resolveAppBindings(rawSelector, options) {
1000
1066
  const selector = rawSelector.trim();
1001
1067
  if (!selector) {
@@ -1014,7 +1080,7 @@ async function resolveAppBindings(rawSelector, options) {
1014
1080
  const displaySelector = current.regionKey ? formatCurrentCfAppSelector(current, selector) : `current/${current.orgName}/${current.spaceName}/${selector}`;
1015
1081
  target = {
1016
1082
  selector: displaySelector,
1017
- apiEndpoint: current.regionKey ? getApiEndpointForRegion(current.regionKey) : void 0,
1083
+ apiEndpoint: current.apiEndpoint,
1018
1084
  orgName: current.orgName,
1019
1085
  spaceName: current.spaceName,
1020
1086
  appName: selector
@@ -1032,10 +1098,16 @@ async function resolveAppBindings(rawSelector, options) {
1032
1098
  if (!apiEndpoint) {
1033
1099
  throw new CfHanaError(
1034
1100
  "CONFIG",
1035
- `Unknown region key "${regionKey}". Use a known region or the current CF target.`
1101
+ `Unknown SAP CF region "${regionKey}". Verify the current SAP region list or use the current CF target.`
1036
1102
  );
1037
1103
  }
1038
- target = { selector, apiEndpoint, orgName, spaceName, appName };
1104
+ let normalizedApiEndpoint;
1105
+ try {
1106
+ normalizedApiEndpoint = normalizeSapCfApiEndpoint(apiEndpoint);
1107
+ } catch (error) {
1108
+ throw new CfHanaError("CONFIG", errorMessageFromUnknown(error), { cause: error });
1109
+ }
1110
+ target = { selector, apiEndpoint: normalizedApiEndpoint, orgName, spaceName, appName };
1039
1111
  }
1040
1112
  let bindings;
1041
1113
  const source = "live";
@@ -1059,9 +1131,6 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1059
1131
  { cause: directError }
1060
1132
  );
1061
1133
  }
1062
- if (!target.apiEndpoint) {
1063
- throw new CfHanaError("CONFIG", "Cannot determine API endpoint for fallback auth.");
1064
- }
1065
1134
  const api = target.apiEndpoint;
1066
1135
  bindings = await withCfSession(async (ctx) => {
1067
1136
  await cfApi(api, ctx);
@@ -1085,9 +1154,6 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1085
1154
  `SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${selector}".`
1086
1155
  );
1087
1156
  }
1088
- if (!target.apiEndpoint) {
1089
- throw new CfHanaError("CONFIG", "Invalid explicit selector.");
1090
- }
1091
1157
  const api = target.apiEndpoint;
1092
1158
  bindings = await withCfSession(async (ctx) => {
1093
1159
  await cfApi(api, ctx);
@@ -1803,6 +1869,57 @@ function boundedRows(rows, requestedLimit) {
1803
1869
  truncated: rows.length > requestedLimit
1804
1870
  };
1805
1871
  }
1872
+ function isBooleanColumn(column) {
1873
+ return column.typeName.trim().toUpperCase() === "BOOLEAN";
1874
+ }
1875
+ function normalizeBooleanCell(value) {
1876
+ if (value === 0) {
1877
+ return false;
1878
+ }
1879
+ if (value === 1) {
1880
+ return true;
1881
+ }
1882
+ if (typeof value !== "string") {
1883
+ return value;
1884
+ }
1885
+ const normalized = value.trim().toLowerCase();
1886
+ if (normalized === "0") {
1887
+ return false;
1888
+ }
1889
+ if (normalized === "1") {
1890
+ return true;
1891
+ }
1892
+ if (normalized === "false") {
1893
+ return false;
1894
+ }
1895
+ if (normalized === "true") {
1896
+ return true;
1897
+ }
1898
+ return value;
1899
+ }
1900
+ function normalizeBooleanRow(row, columns) {
1901
+ let normalizedRow;
1902
+ for (const column of columns) {
1903
+ const value = row[column.name];
1904
+ if (value === void 0) {
1905
+ continue;
1906
+ }
1907
+ const normalizedValue = normalizeBooleanCell(value);
1908
+ if (normalizedValue === value) {
1909
+ continue;
1910
+ }
1911
+ normalizedRow ??= { ...row };
1912
+ normalizedRow[column.name] = normalizedValue;
1913
+ }
1914
+ return normalizedRow ?? row;
1915
+ }
1916
+ function normalizeBooleanRows(rows, columns) {
1917
+ const booleanColumns = columns.filter(isBooleanColumn);
1918
+ if (booleanColumns.length === 0 || rows.length === 0) {
1919
+ return rows;
1920
+ }
1921
+ return rows.map((row) => normalizeBooleanRow(row, booleanColumns));
1922
+ }
1806
1923
  async function withTimeout(work, timeoutMs, onTimeout) {
1807
1924
  let timer;
1808
1925
  const timeout = new Promise((_resolve, reject) => {
@@ -1888,7 +2005,8 @@ var Connection = class _Connection {
1888
2005
  }
1889
2006
  );
1890
2007
  const elapsedMs = Date.now() - started;
1891
- const selected = boundedRows(execResult.rows, limited.requestedLimit);
2008
+ const normalizedRows = normalizeBooleanRows(execResult.rows, execResult.columns);
2009
+ const selected = boundedRows(normalizedRows, limited.requestedLimit);
1892
2010
  return {
1893
2011
  rows: selected.rows,
1894
2012
  columns: execResult.columns,
@@ -3529,25 +3647,6 @@ function parseQualifiedName(value) {
3529
3647
  }
3530
3648
  return { schema: value.slice(0, dot), table: value.slice(dot + 1) };
3531
3649
  }
3532
- async function resolveSelectorArgument(selector) {
3533
- if (selector.includes("/")) {
3534
- return selector;
3535
- }
3536
- const current = await readCurrentCfTarget().catch((error) => {
3537
- throw new CfHanaError(
3538
- "CONFIG",
3539
- "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector.",
3540
- { cause: error }
3541
- );
3542
- });
3543
- if (current === void 0) {
3544
- throw new CfHanaError(
3545
- "CONFIG",
3546
- "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
3547
- );
3548
- }
3549
- return formatCurrentCfAppSelector(current, selector);
3550
- }
3551
3650
  function toConnectOptions(opts) {
3552
3651
  assertPositiveOption("--limit", opts.limit);
3553
3652
  assertPositiveOption("--timeout", opts.timeout);
@@ -3602,7 +3701,7 @@ function formatInfo(info) {
3602
3701
  ].join("\n");
3603
3702
  }
3604
3703
  function withConnectionOptions(command) {
3605
- return command.option("--refresh", "bypass cached credentials and fetch them live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption2).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption2).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption2).option("--no-auto-limit", "disable the automatic SELECT row cap").option("--refresh-metadata", "bypass the 30-minute table/view metadata suggestion cache", false);
3704
+ return command.option("--refresh", "deprecated compatibility flag; binding discovery is already live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption2).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption2).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption2).option("--no-auto-limit", "disable the automatic SELECT row cap").option("--refresh-metadata", "bypass the 30-minute table/view metadata suggestion cache", false);
3606
3705
  }
3607
3706
  function withFormattedConnectionOptions(command) {
3608
3707
  return withConnectionOptions(command).option(
@@ -3690,14 +3789,14 @@ async function runQuery(selector, sql, command) {
3690
3789
  const opts = command.opts();
3691
3790
  assertQueryOptions(sql, opts);
3692
3791
  const cellLimit = resolveCellLimit(opts.cellLimit);
3693
- const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
3792
+ const client = await connect(selector, toConnectOptions(opts));
3694
3793
  try {
3695
3794
  const params = opts.param ?? [];
3696
3795
  let backup;
3697
3796
  try {
3698
3797
  backup = await client.backupWriteStatement(sql, params);
3699
3798
  } catch (error) {
3700
- await enrichAndRethrowQueryError(error, client, sql, opts.refreshMetadata || opts.refresh);
3799
+ await enrichAndRethrowQueryError(error, client, sql, opts.refreshMetadata);
3701
3800
  }
3702
3801
  if (backup !== void 0) {
3703
3802
  process.stderr.write(`${CLI_NAME}: backup saved to ${backup.directory}
@@ -3708,7 +3807,7 @@ async function runQuery(selector, sql, command) {
3708
3807
  error,
3709
3808
  client,
3710
3809
  sql,
3711
- opts.refreshMetadata || opts.refresh
3810
+ opts.refreshMetadata
3712
3811
  );
3713
3812
  });
3714
3813
  if (result.statement === "select") {
@@ -3741,7 +3840,7 @@ async function runQuery(selector, sql, command) {
3741
3840
  }
3742
3841
  async function runTables(selector, schema, command) {
3743
3842
  const opts = command.opts();
3744
- const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
3843
+ const client = await connect(selector, toConnectOptions(opts));
3745
3844
  try {
3746
3845
  const tables = await client.listTables(schema ?? client.info.schema);
3747
3846
  const rows = tables.map((table) => ({
@@ -3757,7 +3856,7 @@ async function runTables(selector, schema, command) {
3757
3856
  async function runColumns(selector, target, command) {
3758
3857
  const opts = command.opts();
3759
3858
  const { schema, table } = parseQualifiedName(target);
3760
- const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
3859
+ const client = await connect(selector, toConnectOptions(opts));
3761
3860
  try {
3762
3861
  const columns = await client.listColumns(schema, table);
3763
3862
  const rows = columns.map((column) => ({
@@ -3775,7 +3874,7 @@ async function runColumns(selector, target, command) {
3775
3874
  async function runCount(selector, target, command) {
3776
3875
  const opts = command.opts();
3777
3876
  const { schema, table } = parseQualifiedName(target);
3778
- const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
3877
+ const client = await connect(selector, toConnectOptions(opts));
3779
3878
  try {
3780
3879
  const total = await client.count({ schema, table });
3781
3880
  print2(String(total));
@@ -3785,7 +3884,7 @@ async function runCount(selector, target, command) {
3785
3884
  }
3786
3885
  async function runPing(selector, command) {
3787
3886
  const opts = command.opts();
3788
- const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
3887
+ const client = await connect(selector, toConnectOptions(opts));
3789
3888
  try {
3790
3889
  const started = Date.now();
3791
3890
  await client.query("SELECT 1 FROM DUMMY");
@@ -3798,7 +3897,7 @@ async function runPing(selector, command) {
3798
3897
  }
3799
3898
  async function runInfo(selector, command) {
3800
3899
  const opts = command.opts();
3801
- const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
3900
+ const client = await connect(selector, toConnectOptions(opts));
3802
3901
  try {
3803
3902
  print2(formatInfo(client.info));
3804
3903
  } finally {