Skip to content

Commit f2c1aff

Browse files
committed
sea-auth: PAT auth flow through SeaBackend → napi binding
SeaBackend.connect() now wires PAT options to the napi binding's openSession(). Non-PAT modes rejected with clear M0-scope error (OAuth/Azure/Federation land in M1). E2E test against pecotesting confirms PAT round-trips: connect → openSession → close all clean. No new dependencies. SeaAuth helper is ~30 LOC. Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
1 parent 3dc4b3c commit f2c1aff

6 files changed

Lines changed: 588 additions & 49 deletions

File tree

lib/sea/SeaAuth.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2026 Databricks, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { ConnectionOptions } from '../contracts/IDBSQLClient';
16+
import AuthenticationError from '../errors/AuthenticationError';
17+
import HiveDriverError from '../errors/HiveDriverError';
18+
19+
/**
20+
* Shape consumed by the napi-binding's `openSession()` (see
21+
* `native/sea/index.d.ts`). M0 supports PAT only — `token` is required.
22+
*
23+
* Mirrors `ConnectionOptions` in the binding's `.d.ts`; declared locally
24+
* to avoid coupling the JS-side adapter to the auto-generated TS file.
25+
*/
26+
export interface SeaNativeConnectionOptions {
27+
hostName: string;
28+
httpPath: string;
29+
token: string;
30+
}
31+
32+
function prependSlash(str: string): string {
33+
if (str.length > 0 && str.charAt(0) !== '/') {
34+
return `/${str}`;
35+
}
36+
return str;
37+
}
38+
39+
/**
40+
* Validate that the user-supplied `ConnectionOptions` describe a PAT auth
41+
* configuration and build the napi-binding's connection-options shape.
42+
*
43+
* M0 SCOPE: PAT only.
44+
* - Accepts `authType: 'access-token'` and the undefined-authType default
45+
* (which already means PAT throughout the existing driver — see
46+
* `DBSQLClient.createAuthProvider`).
47+
* - Rejects every other `authType` discriminant with a clear
48+
* "M0 supports only PAT" message so callers know OAuth / Federation /
49+
* custom providers land in M1.
50+
*
51+
* Throws:
52+
* - `AuthenticationError` when the auth mode is PAT but `token` is missing
53+
* or empty.
54+
* - `HiveDriverError` when the auth mode is anything other than PAT.
55+
*/
56+
export function buildSeaConnectionOptions(options: ConnectionOptions): SeaNativeConnectionOptions {
57+
const { authType } = options as { authType?: string };
58+
59+
if (authType !== undefined && authType !== 'access-token') {
60+
throw new HiveDriverError(
61+
`SEA backend (M0) supports only PAT auth (authType: 'access-token'); ` +
62+
`got authType: '${authType}'. Other auth modes (databricks-oauth, ` +
63+
`token-provider, external-token, static-token, custom) will land in M1.`,
64+
);
65+
}
66+
67+
// PAT path — at this point `options` is structurally the access-token branch
68+
// of `AuthOptions`, which guarantees a `token` field at the type level. We
69+
// still defensively re-check because the public ConnectionOptions type
70+
// permits `authType: undefined` with no token at runtime.
71+
const { token } = options as { token?: string };
72+
if (typeof token !== 'string' || token.length === 0) {
73+
throw new AuthenticationError(
74+
'SEA backend: a non-empty PAT must be supplied via `token` when using `authType: \'access-token\'`.',
75+
);
76+
}
77+
78+
return {
79+
hostName: options.host,
80+
httpPath: prependSlash(options.path),
81+
token,
82+
};
83+
}

lib/sea/SeaBackend.ts

Lines changed: 156 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,169 @@
1+
// Copyright (c) 2026 Databricks, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
115
import IBackend from '../contracts/IBackend';
216
import ISessionBackend from '../contracts/ISessionBackend';
17+
import IOperationBackend from '../contracts/IOperationBackend';
318
import { ConnectionOptions, OpenSessionRequest } from '../contracts/IDBSQLClient';
19+
import {
20+
ExecuteStatementOptions,
21+
TypeInfoRequest,
22+
CatalogsRequest,
23+
SchemasRequest,
24+
TablesRequest,
25+
TableTypesRequest,
26+
ColumnsRequest,
27+
FunctionsRequest,
28+
PrimaryKeysRequest,
29+
CrossReferenceRequest,
30+
} from '../contracts/IDBSQLSession';
31+
import Status from '../dto/Status';
32+
import InfoValue from '../dto/InfoValue';
433
import HiveDriverError from '../errors/HiveDriverError';
34+
import { getSeaNative, SeaNativeBinding } from './SeaNativeLoader';
35+
import { buildSeaConnectionOptions, SeaNativeConnectionOptions } from './SeaAuth';
36+
37+
const NOT_IMPLEMENTED_SESSION =
38+
'SEA session backend: method not implemented in sea-auth (M0); lands in sea-execution/sea-operation.';
39+
40+
/**
41+
* Opaque handle to the napi binding's `Connection` class. The exact
42+
* shape lives in `native/sea/index.d.ts` (auto-generated). We type it as
43+
* a structural minimum here so the loader's pass-through typing doesn't
44+
* leak into every call site.
45+
*/
46+
interface NativeConnection {
47+
close(): Promise<void>;
48+
}
49+
50+
/**
51+
* Minimal `ISessionBackend` that wraps the napi-binding's `Connection`.
52+
*
53+
* For M0 (sea-auth) only `id` and `close()` are functional — they're the
54+
* subset required to round-trip a connect-open-close cycle. Every other
55+
* method throws a clear "not implemented in M0" `HiveDriverError`.
56+
*
57+
* The `id` field is currently a synthetic counter-based string; the kernel
58+
* exposes a real session-id through a follow-on getter that
59+
* `sea-execution` will wire through.
60+
*/
61+
export class SeaSessionBackend implements ISessionBackend {
62+
private static seq = 0;
63+
64+
public readonly id: string;
65+
66+
private readonly connection: NativeConnection;
67+
68+
constructor(connection: NativeConnection) {
69+
this.connection = connection;
70+
SeaSessionBackend.seq += 1;
71+
this.id = `sea-session-${SeaSessionBackend.seq}`;
72+
}
73+
74+
/* eslint-disable @typescript-eslint/no-unused-vars */
75+
public async getInfo(_infoType: number): Promise<InfoValue> {
76+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
77+
}
78+
79+
public async executeStatement(
80+
_statement: string,
81+
_options: ExecuteStatementOptions,
82+
): Promise<IOperationBackend> {
83+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
84+
}
85+
86+
public async getTypeInfo(_request: TypeInfoRequest): Promise<IOperationBackend> {
87+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
88+
}
89+
90+
public async getCatalogs(_request: CatalogsRequest): Promise<IOperationBackend> {
91+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
92+
}
93+
94+
public async getSchemas(_request: SchemasRequest): Promise<IOperationBackend> {
95+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
96+
}
597

6-
const NOT_IMPLEMENTED = 'SEA backend not implemented yet — wired in sea-napi-binding feature';
98+
public async getTables(_request: TablesRequest): Promise<IOperationBackend> {
99+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
100+
}
101+
102+
public async getTableTypes(_request: TableTypesRequest): Promise<IOperationBackend> {
103+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
104+
}
105+
106+
public async getColumns(_request: ColumnsRequest): Promise<IOperationBackend> {
107+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
108+
}
7109

110+
public async getFunctions(_request: FunctionsRequest): Promise<IOperationBackend> {
111+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
112+
}
113+
114+
public async getPrimaryKeys(_request: PrimaryKeysRequest): Promise<IOperationBackend> {
115+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
116+
}
117+
118+
public async getCrossReference(_request: CrossReferenceRequest): Promise<IOperationBackend> {
119+
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
120+
}
121+
/* eslint-enable @typescript-eslint/no-unused-vars */
122+
123+
public async close(): Promise<Status> {
124+
await this.connection.close();
125+
return Status.success();
126+
}
127+
}
128+
129+
/**
130+
* M0 SeaBackend — wires PAT auth + napi `openSession` end-to-end.
131+
*
132+
* Connect is a no-op at this layer (the napi binding has no notion of a
133+
* standalone "connect"; a session is opened directly). We capture the
134+
* validated PAT options and hand them to `openSession()` on demand.
135+
*
136+
* Subsequent milestones (`sea-execution`, `sea-operation`) replace the
137+
* stubbed `ISessionBackend` / `IOperationBackend` methods with real
138+
* napi-binding calls.
139+
*/
8140
export default class SeaBackend implements IBackend {
9-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
141+
private nativeOptions?: SeaNativeConnectionOptions;
142+
143+
private readonly native: SeaNativeBinding;
144+
145+
constructor(native: SeaNativeBinding = getSeaNative()) {
146+
this.native = native;
147+
}
148+
10149
public async connect(options: ConnectionOptions): Promise<void> {
11-
throw new HiveDriverError(NOT_IMPLEMENTED);
150+
// Validate PAT auth + capture the napi-binding option shape.
151+
// Any non-PAT mode (or a missing token) throws here, before we ever
152+
// touch the native binding.
153+
this.nativeOptions = buildSeaConnectionOptions(options);
12154
}
13155

14-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
15-
public async openSession(request: OpenSessionRequest): Promise<ISessionBackend> {
16-
throw new HiveDriverError(NOT_IMPLEMENTED);
156+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
157+
public async openSession(_request: OpenSessionRequest): Promise<ISessionBackend> {
158+
if (!this.nativeOptions) {
159+
throw new HiveDriverError('SeaBackend: connect() must be called before openSession().');
160+
}
161+
const connection = (await this.native.openSession(this.nativeOptions)) as NativeConnection;
162+
return new SeaSessionBackend(connection);
17163
}
18164

19-
// No-op so DBSQLClient.close() can finish its state-clearing block after a
20-
// failed useSEA: true connect. Real teardown lands with the M1 SEA impl.
21-
// eslint-disable-next-line @typescript-eslint/no-empty-function, class-methods-use-this
22-
public async close(): Promise<void> {}
165+
public async close(): Promise<void> {
166+
// Connection-level resources are owned by the session wrapper. No-op here.
167+
this.nativeOptions = undefined;
168+
}
23169
}

tests/integration/.mocharc.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
const allSpecs = 'tests/integration/**/*.test.ts';
4+
5+
const argvSpecs = process.argv.slice(4);
6+
7+
module.exports = {
8+
spec: argvSpecs.length > 0 ? argvSpecs : allSpecs,
9+
timeout: '300000',
10+
require: ['ts-node/register'],
11+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2026 Databricks, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { expect } from 'chai';
16+
import { DBSQLClient } from '../../../lib';
17+
18+
/**
19+
* sea-auth M0 end-to-end:
20+
* 1. Construct a DBSQLClient.
21+
* 2. `connect({ useSEA: true, token })` against pecotesting.
22+
* 3. `openSession()` — round-trips through the napi binding.
23+
* 4. Close the session, then the client.
24+
*
25+
* No query is executed here — execution is the responsibility of the
26+
* sea-execution feature's own e2e. This test exists solely to confirm
27+
* the PAT round-trips end-to-end and the napi binding's `openSession`
28+
* surface is reachable from `DBSQLClient`.
29+
*
30+
* Required env (exported by `~/.zshrc` on the developer machine):
31+
* - DATABRICKS_PECOTESTING_SERVER_HOSTNAME
32+
* - DATABRICKS_PECOTESTING_HTTP_PATH
33+
* - DATABRICKS_PECOTESTING_TOKEN_PERSONAL (preferred — personal PAT)
34+
* - DATABRICKS_PECOTESTING_TOKEN (fallback — shared PAT)
35+
*
36+
* If any of the three required env vars is missing, the suite is skipped
37+
* so CI machines without secrets don't fail-flap.
38+
*/
39+
describe('sea-auth e2e — PAT through DBSQLClient ↔ SeaBackend ↔ napi binding', function suite() {
40+
const host = process.env.DATABRICKS_PECOTESTING_SERVER_HOSTNAME;
41+
const path = process.env.DATABRICKS_PECOTESTING_HTTP_PATH;
42+
const token =
43+
process.env.DATABRICKS_PECOTESTING_TOKEN_PERSONAL || process.env.DATABRICKS_PECOTESTING_TOKEN;
44+
45+
this.timeout(120_000);
46+
47+
before(function gate() {
48+
if (!host || !path || !token) {
49+
// eslint-disable-next-line no-invalid-this
50+
this.skip();
51+
}
52+
});
53+
54+
it('connects, opens a session, closes the session, closes the client', async () => {
55+
const client = new DBSQLClient();
56+
57+
const connected = await client.connect({
58+
host: host as string,
59+
path: path as string,
60+
token: token as string,
61+
useSEA: true,
62+
});
63+
expect(connected).to.equal(client);
64+
65+
const session = await client.openSession();
66+
expect(session).to.exist;
67+
expect(session.id).to.be.a('string');
68+
expect(session.id.length).to.be.greaterThan(0);
69+
70+
const status = await session.close();
71+
expect(status.isSuccess).to.equal(true);
72+
73+
await client.close();
74+
});
75+
});

tests/unit/sea/SeaBackend.test.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)