|
13 | 13 | // limitations under the License. |
14 | 14 |
|
15 | 15 | import { ConnectionOptions } from '../contracts/IDBSQLClient'; |
| 16 | +import { InternalConnectionOptions } from '../contracts/InternalConnectionOptions'; |
16 | 17 | import AuthenticationError from '../errors/AuthenticationError'; |
17 | 18 | import HiveDriverError from '../errors/HiveDriverError'; |
18 | 19 |
|
@@ -66,9 +67,58 @@ export interface SeaSessionDefaults { |
66 | 67 | catalog?: string; |
67 | 68 | schema?: string; |
68 | 69 | sessionConf?: Record<string, string>; |
| 70 | + /** |
| 71 | + * Render `INTERVAL` / `DURATION` result columns as strings |
| 72 | + * (kernel `ResultConfig.intervals_as_string`). The kernel default is |
| 73 | + * native Arrow `month_interval` / `duration[us]`, but the NodeJS |
| 74 | + * Thrift driver surfaces intervals as strings — so the SEA path sets |
| 75 | + * this `true` so its result shape is a byte-compatible drop-in for the |
| 76 | + * Thrift backend. Omitting it falls back to the kernel's native types. |
| 77 | + */ |
| 78 | + intervalsAsString?: boolean; |
| 79 | + /** |
| 80 | + * Render complex (`ARRAY` / `MAP` / `STRUCT` / `VARIANT`) result |
| 81 | + * columns as JSON strings (kernel `ResultConfig.complex_types_as_json`). |
| 82 | + * Left unset on the SEA path: native Arrow nested types already decode |
| 83 | + * identically to the Thrift backend through the shared Arrow converter, |
| 84 | + * so forcing JSON here would *introduce* a divergence rather than |
| 85 | + * remove one. |
| 86 | + */ |
| 87 | + complexTypesAsJson?: boolean; |
| 88 | + /** |
| 89 | + * Per-session kernel connection-pool size |
| 90 | + * (kernel `ConnectionOptions.max_connections`). Validated as a positive |
| 91 | + * integer within the napi `u32` range by `buildSeaConnectionOptions`. |
| 92 | + */ |
| 93 | + maxConnections?: number; |
| 94 | +} |
| 95 | + |
| 96 | +/** |
| 97 | + * TLS options shared across all auth-mode variants. Mirror the napi |
| 98 | + * binding's `ConnectionOptions.checkServerCertificate` / `.customCaCert` |
| 99 | + * (kernel `Session::builder().tls(TlsConfig)`). |
| 100 | + * |
| 101 | + * The napi shape takes `customCaCert` as a `Buffer` only; the public |
| 102 | + * `ConnectionOptions` additionally accepts a PEM string, which |
| 103 | + * `buildSeaConnectionOptions` normalises to a `Buffer` before crossing |
| 104 | + * the FFI boundary. |
| 105 | + */ |
| 106 | +export interface SeaTlsOptions { |
| 107 | + /** |
| 108 | + * Verify the server's TLS certificate. The SEA backend is |
| 109 | + * **secure-by-default**: omitting this leaves the kernel default of |
| 110 | + * `true` (full chain + hostname verification). Set `false` only to opt |
| 111 | + * into the insecure, accept-anything mode (analogous to Thrift's |
| 112 | + * `rejectUnauthorized: false`); prefer pairing strict checking with |
| 113 | + * `customCaCert` over disabling verification entirely. |
| 114 | + */ |
| 115 | + checkServerCertificate?: boolean; |
| 116 | + /** PEM-encoded CA bytes to add to the trust store. */ |
| 117 | + customCaCert?: Buffer; |
69 | 118 | } |
70 | 119 |
|
71 | 120 | export type SeaNativeConnectionOptions = SeaSessionDefaults & |
| 121 | + SeaTlsOptions & |
72 | 122 | ( |
73 | 123 | | { |
74 | 124 | hostName: string; |
@@ -114,6 +164,64 @@ export function isBlankOrReserved(s: string): boolean { |
114 | 164 | return normalized.length === 0 || normalized === 'undefined' || normalized === 'null'; |
115 | 165 | } |
116 | 166 |
|
| 167 | +/** napi-rs marshals `maxConnections` as a `u32`; reject values it can't hold. */ |
| 168 | +const MAX_U32 = 0xffffffff; |
| 169 | + |
| 170 | +/** |
| 171 | + * Normalise the public TLS options (`checkServerCertificate` / |
| 172 | + * `customCaCert`) into the napi shape. |
| 173 | + * |
| 174 | + * - `checkServerCertificate` passes through verbatim (only when set; an |
| 175 | + * absent value leaves the kernel default, which is secure — verify on). |
| 176 | + * - `customCaCert` accepts a PEM string or `Buffer` on the public |
| 177 | + * surface; we convert a string to a `Buffer` here and do a light PEM |
| 178 | + * sanity check. The bytes are NOT parsed in JS — the kernel returns a |
| 179 | + * meaningful error if the PEM is malformed. |
| 180 | + * |
| 181 | + * Throws `HiveDriverError` when `customCaCert` is supplied but empty or |
| 182 | + * (for strings) lacks a PEM certificate header. |
| 183 | + */ |
| 184 | +export function buildSeaTlsOptions(options: ConnectionOptions): SeaTlsOptions { |
| 185 | + // Read the SEA-only fields through the purpose-built internal options type |
| 186 | + // rather than an ad-hoc inline cast, so the shape can't silently drift from |
| 187 | + // its declaration and a typo'd key fails to compile. |
| 188 | + const { checkServerCertificate, customCaCert } = options as ConnectionOptions & InternalConnectionOptions; |
| 189 | + |
| 190 | + const tls: SeaTlsOptions = {}; |
| 191 | + |
| 192 | + if (checkServerCertificate !== undefined) { |
| 193 | + tls.checkServerCertificate = checkServerCertificate; |
| 194 | + } |
| 195 | + |
| 196 | + if (customCaCert !== undefined) { |
| 197 | + if (typeof customCaCert === 'string') { |
| 198 | + // Light PEM sanity check — require both the BEGIN and END markers so a |
| 199 | + // truncated/headerless cert is rejected here rather than surfacing as an |
| 200 | + // opaque kernel TLS error. Full parsing is deferred to the kernel. |
| 201 | + if ( |
| 202 | + !customCaCert.includes('-----BEGIN CERTIFICATE-----') || |
| 203 | + !customCaCert.includes('-----END CERTIFICATE-----') |
| 204 | + ) { |
| 205 | + throw new HiveDriverError( |
| 206 | + 'SEA backend: `customCaCert` string does not look like a PEM certificate ' + |
| 207 | + "(missing the '-----BEGIN CERTIFICATE-----' / '-----END CERTIFICATE-----' markers). " + |
| 208 | + 'Pass PEM text or a Buffer of PEM bytes.', |
| 209 | + ); |
| 210 | + } |
| 211 | + tls.customCaCert = Buffer.from(customCaCert, 'utf8'); |
| 212 | + } else if (Buffer.isBuffer(customCaCert)) { |
| 213 | + if (customCaCert.length === 0) { |
| 214 | + throw new HiveDriverError('SEA backend: `customCaCert` Buffer is empty.'); |
| 215 | + } |
| 216 | + tls.customCaCert = customCaCert; |
| 217 | + } else { |
| 218 | + throw new HiveDriverError('SEA backend: `customCaCert` must be a PEM string or a Buffer.'); |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + return tls; |
| 223 | +} |
| 224 | + |
117 | 225 | /** |
118 | 226 | * Validate the user-supplied `ConnectionOptions` and build the |
119 | 227 | * napi-binding's connection-options shape. |
@@ -170,11 +278,43 @@ export function isBlankOrReserved(s: string): boolean { |
170 | 278 | export function buildSeaConnectionOptions(options: ConnectionOptions): SeaNativeConnectionOptions { |
171 | 279 | const { authType } = options as { authType?: string }; |
172 | 280 |
|
173 | | - const base = { |
| 281 | + const base: { |
| 282 | + hostName: string; |
| 283 | + httpPath: string; |
| 284 | + intervalsAsString: boolean; |
| 285 | + maxConnections?: number; |
| 286 | + } & SeaTlsOptions = { |
174 | 287 | hostName: options.host, |
175 | 288 | httpPath: prependSlash(options.path), |
| 289 | + // Match the NodeJS Thrift driver, which surfaces INTERVAL columns as |
| 290 | + // strings. The kernel defaults to native Arrow interval/duration types; |
| 291 | + // forcing the string rendering here keeps the SEA path a byte-compatible |
| 292 | + // drop-in. Complex types are intentionally left at the kernel default |
| 293 | + // (native Arrow) — they already decode identically to Thrift via the |
| 294 | + // shared Arrow converter, so `complexTypesAsJson` is not forced on. |
| 295 | + intervalsAsString: true, |
| 296 | + // TLS knobs (server-cert verification toggle + custom CA). Validated and |
| 297 | + // normalised (string PEM → Buffer) here so the napi shape only sees a Buffer. |
| 298 | + ...buildSeaTlsOptions(options), |
176 | 299 | }; |
177 | 300 |
|
| 301 | + // SEA-only pool sizing; read via cast to match how this function reads the |
| 302 | + // other SEA-specific options (TLS) — they live on the internal options |
| 303 | + // surface, not the published public `ConnectionOptions` `.d.ts`. |
| 304 | + const { maxConnections } = options as ConnectionOptions & InternalConnectionOptions; |
| 305 | + if (maxConnections !== undefined) { |
| 306 | + if (!Number.isInteger(maxConnections) || maxConnections < 1) { |
| 307 | + throw new HiveDriverError(`SEA backend: \`maxConnections\` must be a positive integer; got ${maxConnections}.`); |
| 308 | + } |
| 309 | + if (maxConnections > MAX_U32) { |
| 310 | + throw new HiveDriverError( |
| 311 | + `SEA backend: \`maxConnections\` exceeds the napi u32 limit (${MAX_U32}); got ${maxConnections}. ` + |
| 312 | + 'Typical pool sizes are 10-500.', |
| 313 | + ); |
| 314 | + } |
| 315 | + base.maxConnections = maxConnections; |
| 316 | + } |
| 317 | + |
178 | 318 | const oauth = options as { |
179 | 319 | oauthClientId?: string; |
180 | 320 | oauthClientSecret?: string; |
|
0 commit comments