@dbx-tools/auth 0.6.182 → 0.9.28

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.
Files changed (53) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +116 -32
  3. package/index.ts +26 -2
  4. package/lib/exports.d.ts +2 -0
  5. package/lib/exports.js +3 -0
  6. package/lib/index.d.ts +27 -0
  7. package/lib/index.js +26 -1595
  8. package/lib/src/_logging.d.ts +10 -0
  9. package/lib/src/_logging.js +38 -0
  10. package/lib/src/_profile-core.d.ts +28 -0
  11. package/lib/src/_profile-core.js +216 -0
  12. package/lib/src/_profile.d.ts +15 -0
  13. package/lib/src/_profile.js +236 -0
  14. package/lib/src/databricks-auth.d.ts +11 -0
  15. package/lib/src/databricks-auth.js +314 -0
  16. package/lib/src/databricks-cli.d.ts +28 -0
  17. package/lib/src/databricks-cli.js +239 -0
  18. package/lib/src/errors.d.ts +7 -0
  19. package/lib/src/errors.js +14 -0
  20. package/lib/src/generated/databricks-cli-assets.json +33 -0
  21. package/lib/src/http-client.d.ts +23 -0
  22. package/lib/src/http-client.js +116 -0
  23. package/lib/src/lifecycle.d.ts +26 -0
  24. package/lib/src/lifecycle.js +229 -0
  25. package/lib/src/node-storage.d.ts +20 -0
  26. package/lib/src/node-storage.js +207 -0
  27. package/lib/src/personal-access-token.d.ts +11 -0
  28. package/lib/src/personal-access-token.js +30 -0
  29. package/lib/src/service-principal.d.ts +24 -0
  30. package/lib/src/service-principal.js +69 -0
  31. package/lib/src/storage.d.ts +20 -0
  32. package/lib/src/storage.js +98 -0
  33. package/lib/src/types.d.ts +157 -0
  34. package/lib/src/types.js +67 -0
  35. package/lib/tsconfig.tsbuildinfo +1 -0
  36. package/package.json +39 -16
  37. package/src/_logging.ts +42 -0
  38. package/src/_profile-core.ts +258 -0
  39. package/src/_profile.ts +319 -0
  40. package/src/databricks-auth.ts +416 -0
  41. package/src/databricks-cli.ts +298 -0
  42. package/src/errors.ts +16 -0
  43. package/src/generated/databricks-cli-assets.json +33 -0
  44. package/src/http-client.ts +147 -0
  45. package/src/lifecycle.ts +239 -0
  46. package/src/node-storage.ts +230 -0
  47. package/src/personal-access-token.ts +34 -0
  48. package/src/service-principal.ts +96 -0
  49. package/src/storage.ts +104 -0
  50. package/src/types.ts +188 -0
  51. package/src/_bindings-ffi.ts +0 -715
  52. package/src/_bindings.ts +0 -1794
  53. package/src/bindings.ts +0 -12
@@ -0,0 +1,298 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ import * as bin from "@dbx-tools/core/bin";
5
+ import * as exec from "@dbx-tools/core/exec";
6
+ import * as stringUtils from "@dbx-tools/shared-core/string-utils";
7
+
8
+ import cliAssets from "./generated/databricks-cli-assets.json" with { type: "json" };
9
+ import { authLogger, failureMetadata, tokenMetadata } from "./_logging.ts";
10
+ import { AuthError } from "./errors.ts";
11
+ import type { Token, TokenProvider } from "./types.ts";
12
+
13
+ const resolutionCache = new Map<string, Promise<string | undefined>>();
14
+ const logger = authLogger("cli");
15
+
16
+ interface DatabricksCliAsset {
17
+ readonly url: string;
18
+ readonly sha256: string;
19
+ }
20
+
21
+ /** Deferred Databricks CLI lookup used by U2M token acquisition. */
22
+ export type DatabricksCliResolver = () => Promise<string | undefined>;
23
+
24
+ /** Controls whether CLI resolution may install the pinned managed executable. */
25
+ export interface DatabricksCliResolutionOptions {
26
+ install?: boolean;
27
+ }
28
+
29
+ /** Resolve a compatible Databricks CLI, installing the pinned build asset when needed. */
30
+ export function resolveDatabricksCli(
31
+ environment: Readonly<Record<string, string | undefined>> = process.env,
32
+ options: DatabricksCliResolutionOptions = {},
33
+ ): Promise<string | undefined> {
34
+ const candidate = stringValue(environment.DATABRICKS_CLI_PATH) ?? "databricks";
35
+ const key = `${candidate}\0${homedir()}\0${process.platform}\0${process.arch}\0${options.install !== false}`;
36
+ const cached = resolutionCache.get(key);
37
+ if (cached) {
38
+ logger.debug("reused Databricks CLI resolution", {
39
+ candidate,
40
+ install: options.install !== false,
41
+ });
42
+ return cached;
43
+ }
44
+ logger.debug("resolving Databricks CLI", {
45
+ candidate,
46
+ install: options.install !== false,
47
+ platform: process.platform,
48
+ arch: process.arch,
49
+ minimumVersion: cliAssets.minimumVersion,
50
+ });
51
+ const resolved = resolveDatabricksCliUncached(candidate, options.install !== false).catch(
52
+ (error) => {
53
+ resolutionCache.delete(key);
54
+ throw error;
55
+ },
56
+ );
57
+ resolutionCache.set(key, resolved);
58
+ return resolved;
59
+ }
60
+
61
+ /** Clear cached CLI resolution, primarily after installation or environment changes. */
62
+ export function resetDatabricksCliResolution(): void {
63
+ resolutionCache.clear();
64
+ logger.debug("cleared Databricks CLI resolution cache");
65
+ }
66
+
67
+ async function resolveDatabricksCliUncached(
68
+ candidate: string,
69
+ install: boolean,
70
+ ): Promise<string | undefined> {
71
+ if (await compatibleDatabricksCli(candidate)) {
72
+ logger.debug("selected installed Databricks CLI", { executable: candidate });
73
+ return candidate;
74
+ }
75
+ const managed = managedExecutable();
76
+ if (await compatibleDatabricksCli(managed)) {
77
+ logger.debug("selected managed Databricks CLI", { executable: managed });
78
+ return managed;
79
+ }
80
+ if (!install) {
81
+ logger.debug("compatible Databricks CLI unavailable and installation disabled");
82
+ return undefined;
83
+ }
84
+ const asset = platformAsset();
85
+ if (!asset) {
86
+ logger.debug("no managed Databricks CLI asset for platform", {
87
+ platform: process.platform,
88
+ arch: process.arch,
89
+ });
90
+ return undefined;
91
+ }
92
+ const root = join(homedir(), ".databricks");
93
+ const binDir = join(root, "bin");
94
+ const executable = managedExecutableName();
95
+ const installed = await bin.ensure("databricks", asset, {
96
+ autoUnpackage: true,
97
+ destination: { root, binDir, path: join(binDir, executable) },
98
+ minVersion: cliAssets.minimumVersion,
99
+ selector: ({ source }) => join(source, executable),
100
+ versionParser: (output) => {
101
+ const version = bin.parseVersion(output);
102
+ return version === cliAssets.version ? version : undefined;
103
+ },
104
+ });
105
+ logger.debug("ensured managed Databricks CLI", {
106
+ executable: installed.path,
107
+ version: cliAssets.version,
108
+ });
109
+ return installed.path;
110
+ }
111
+
112
+ function managedExecutable(): string {
113
+ return join(homedir(), ".databricks", "bin", managedExecutableName());
114
+ }
115
+
116
+ function managedExecutableName(): string {
117
+ return process.platform === "win32" ? "databricks.exe" : "databricks";
118
+ }
119
+
120
+ async function compatibleDatabricksCli(executable: string): Promise<boolean> {
121
+ try {
122
+ const result = await runProcess(executable, ["--version"], {}, 10_000);
123
+ if (result.exitCode !== 0) {
124
+ logger.debug("Databricks CLI version probe failed", {
125
+ executable,
126
+ exitCode: result.exitCode,
127
+ });
128
+ return false;
129
+ }
130
+ const version = bin.parseVersion(result);
131
+ const compatible =
132
+ version !== undefined && bin.isVersionAtLeast(version, cliAssets.minimumVersion);
133
+ logger.debug("probed Databricks CLI version", {
134
+ executable,
135
+ version: version ?? "unknown",
136
+ minimumVersion: cliAssets.minimumVersion,
137
+ compatible,
138
+ });
139
+ return compatible;
140
+ } catch (cause) {
141
+ logger.debug("Databricks CLI version probe unavailable", {
142
+ executable,
143
+ error: failureMetadata(cause),
144
+ });
145
+ return false;
146
+ }
147
+ }
148
+
149
+ function platformAsset(): DatabricksCliAsset | undefined {
150
+ const key = `${process.platform}-${process.arch}` as keyof typeof cliAssets.assets;
151
+ return cliAssets.assets[key];
152
+ }
153
+
154
+ /** Run interactive Databricks CLI login for one profile. */
155
+ export async function databricksCliLogin(
156
+ profile: string,
157
+ timeoutMs: number,
158
+ executable = process.env.DATABRICKS_CLI_PATH ?? "databricks",
159
+ environment: Record<string, string> = {},
160
+ ): Promise<void> {
161
+ logger.debug("starting Databricks CLI login", { profile, executable, timeoutMs });
162
+ const result = await runProcess(
163
+ executable,
164
+ ["auth", "login", "--profile", profile, "--timeout", `${Math.ceil(timeoutMs / 1000)}s`],
165
+ environment,
166
+ );
167
+ if (result.exitCode !== 0)
168
+ throw new AuthError("cli", result.stderr || `databricks auth login exited ${result.exitCode}`);
169
+ logger.debug("Databricks CLI login completed", { profile, executable });
170
+ }
171
+
172
+ /** Request one profile token from the Databricks CLI. */
173
+ export async function databricksCliToken(
174
+ profile: string,
175
+ forceRefresh = false,
176
+ executable = process.env.DATABRICKS_CLI_PATH ?? "databricks",
177
+ environment: Record<string, string> = {},
178
+ ): Promise<Token> {
179
+ logger.debug("requesting Databricks CLI token", { profile, executable, forceRefresh });
180
+ const args = ["auth", "token", "--profile", profile, "--output", "json"];
181
+ if (forceRefresh) args.push("--force-refresh");
182
+ const result = await runProcess(executable, args, environment);
183
+ if (result.exitCode !== 0)
184
+ throw new AuthError("cli", result.stderr || `databricks auth token exited ${result.exitCode}`);
185
+ let value: Record<string, unknown>;
186
+ try {
187
+ value = JSON.parse(result.stdout ?? "") as Record<string, unknown>;
188
+ } catch (cause) {
189
+ throw new AuthError("cli", "Databricks CLI token output was not JSON", { cause });
190
+ }
191
+ const accessToken = stringValue(value.access_token ?? value.accessToken);
192
+ if (!accessToken) throw new AuthError("cli", "Databricks CLI token output had no access token");
193
+ const token = {
194
+ accessToken,
195
+ tokenType: stringValue(value.token_type ?? value.tokenType) ?? "Bearer",
196
+ ...(stringValue(value.refresh_token ?? value.refreshToken)
197
+ ? { refreshToken: stringValue(value.refresh_token ?? value.refreshToken) }
198
+ : {}),
199
+ ...(stringValue(value.expiry ?? value.expires_at)
200
+ ? { expiry: stringValue(value.expiry ?? value.expires_at) }
201
+ : {}),
202
+ scopes: Array.isArray(value.scopes)
203
+ ? value.scopes.filter((scope): scope is string => typeof scope === "string")
204
+ : [],
205
+ };
206
+ logger.debug("received Databricks CLI token", {
207
+ profile,
208
+ forceRefresh,
209
+ token: tokenMetadata(token),
210
+ });
211
+ return token;
212
+ }
213
+
214
+ async function runProcess(
215
+ command: string,
216
+ args: string[],
217
+ environment: Record<string, string>,
218
+ timeoutMs?: number,
219
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
220
+ const controller = timeoutMs === undefined ? undefined : new AbortController();
221
+ const timeout = controller ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
222
+ try {
223
+ return await exec.spawn(command, args, {
224
+ env: { ...process.env, ...environment },
225
+ stdin: "ignore",
226
+ stdout: "capture",
227
+ stderr: "capture",
228
+ signal: controller?.signal,
229
+ });
230
+ } finally {
231
+ if (timeout) clearTimeout(timeout);
232
+ }
233
+ }
234
+
235
+ /** Provider that lazily delegates U2M credentials to the Databricks CLI. */
236
+ export class DatabricksCliProvider implements TokenProvider {
237
+ private resolution?: Promise<string>;
238
+
239
+ constructor(
240
+ private readonly profile: string,
241
+ private readonly executableOrResolver: string | DatabricksCliResolver = () =>
242
+ resolveDatabricksCli(),
243
+ private readonly environment: Record<string, string> = {},
244
+ ) {}
245
+
246
+ async authenticate(_timeoutMs: number): Promise<Token> {
247
+ return databricksCliToken(
248
+ this.profile,
249
+ false,
250
+ await this.resolveExecutable(),
251
+ this.environment,
252
+ );
253
+ }
254
+
255
+ async login(timeoutMs: number): Promise<Token> {
256
+ const executable = await this.resolveExecutable();
257
+ await databricksCliLogin(this.profile, timeoutMs, executable, this.environment);
258
+ return databricksCliToken(this.profile, false, executable, this.environment);
259
+ }
260
+
261
+ async refresh(_token: Token): Promise<Token> {
262
+ return databricksCliToken(this.profile, true, await this.resolveExecutable(), this.environment);
263
+ }
264
+
265
+ canAuthenticateSilently(): boolean {
266
+ return true;
267
+ }
268
+
269
+ private resolveExecutable(): Promise<string> {
270
+ if (typeof this.executableOrResolver === "string") {
271
+ logger.debug("using configured Databricks CLI executable", {
272
+ profile: this.profile,
273
+ executable: this.executableOrResolver,
274
+ });
275
+ return Promise.resolve(this.executableOrResolver);
276
+ }
277
+ if (this.resolution) {
278
+ logger.debug("reused provider CLI resolution", { profile: this.profile });
279
+ return this.resolution;
280
+ }
281
+ logger.debug("resolving provider CLI lazily", { profile: this.profile });
282
+ this.resolution = this.executableOrResolver()
283
+ .then((executable) => {
284
+ if (!executable)
285
+ throw new AuthError("cli", "Databricks CLI is unavailable on this platform");
286
+ return executable;
287
+ })
288
+ .catch((error) => {
289
+ this.resolution = undefined;
290
+ throw error;
291
+ });
292
+ return this.resolution;
293
+ }
294
+ }
295
+
296
+ function stringValue(value: unknown): string | undefined {
297
+ return stringUtils.trimToNull(value) ?? undefined;
298
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** Authentication failure with a stable category for callers and language shims. */
2
+ export class AuthError extends Error {
3
+ constructor(
4
+ readonly kind: "config" | "oauth" | "storage" | "lock-timeout" | "cli" | "http",
5
+ message: string,
6
+ options?: ErrorOptions,
7
+ ) {
8
+ super(message, options);
9
+ this.name = "AuthError";
10
+ }
11
+ }
12
+
13
+ /** Normalize arbitrary failures without erasing their original cause. */
14
+ export function authError(kind: AuthError["kind"], message: string, cause?: unknown): AuthError {
15
+ return cause instanceof AuthError ? cause : new AuthError(kind, message, { cause });
16
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "dbxToolsVersion": "0.9.21",
4
+ "minimumVersion": "0.296.0",
5
+ "version": "1.19.0",
6
+ "publishedAt": "2026-09-30T13:13:22Z",
7
+ "assets": {
8
+ "darwin-arm64": {
9
+ "url": "https://github.com/databricks/cli/releases/download/v1.19.0/databricks_cli_1.19.0_darwin_arm64.tar.gz",
10
+ "sha256": "b84c6a8c623578308a724cdd7181b92b0816c2531564f16f8de7559b2a39f51f"
11
+ },
12
+ "darwin-x64": {
13
+ "url": "https://github.com/databricks/cli/releases/download/v1.19.0/databricks_cli_1.19.0_darwin_amd64.tar.gz",
14
+ "sha256": "d29a5dc1ea7d44d0d892fd4beac05d22daa932982ca6bf98a06d52c95f5ed4cd"
15
+ },
16
+ "linux-arm64": {
17
+ "url": "https://github.com/databricks/cli/releases/download/v1.19.0/databricks_cli_1.19.0_linux_arm64.tar.gz",
18
+ "sha256": "2997ccbda57090ee48bd9282d5cb3a5bef78025db64fd1573fbd7c8726856086"
19
+ },
20
+ "linux-x64": {
21
+ "url": "https://github.com/databricks/cli/releases/download/v1.19.0/databricks_cli_1.19.0_linux_amd64.tar.gz",
22
+ "sha256": "bddf87df3d72b61384da724b40f18ab483a98af742748260097a7bb8154b070e"
23
+ },
24
+ "win32-arm64": {
25
+ "url": "https://github.com/databricks/cli/releases/download/v1.19.0/databricks_cli_1.19.0_windows_arm64.tar.gz",
26
+ "sha256": "c28d098490ea1966230c07585312f8bc66e18d468e34b06319d44e29d61c4602"
27
+ },
28
+ "win32-x64": {
29
+ "url": "https://github.com/databricks/cli/releases/download/v1.19.0/databricks_cli_1.19.0_windows_amd64.tar.gz",
30
+ "sha256": "0ded24a2b001e001c815b9c1c66dc2ef1f6c2e60c64f63c6d9afb8e9fe85afdb"
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,147 @@
1
+ import { authLogger } from "./_logging.ts";
2
+ import { AuthError } from "./errors.ts";
3
+ import { createAuthClient, type DatabricksAuthDependencies } from "./databricks-auth.ts";
4
+ import type { AuthClient, DatabricksAuthOptions } from "./types.ts";
5
+ import { DEFAULT_ACCESS_TOKEN_HEADER, WORKSPACE_ID_HEADER } from "./types.ts";
6
+
7
+ const logger = authLogger("http");
8
+
9
+ /** Options for one Databricks JSON API request. */
10
+ export interface DatabricksRequestOptions {
11
+ method?: string;
12
+ body?: unknown;
13
+ headers?: Headers | Record<string, string> | [string, string][];
14
+ login?: boolean;
15
+ signal?: AbortSignal;
16
+ }
17
+
18
+ /** Dependency-light Databricks JSON client with one rejected-token refresh. */
19
+ export class DatabricksClient {
20
+ private constructor(
21
+ readonly auth: AuthClient,
22
+ private readonly fetcher: typeof globalThis.fetch,
23
+ ) {}
24
+
25
+ static async create(
26
+ options: DatabricksAuthOptions = { preferUserToMachine: true },
27
+ dependencies: DatabricksAuthDependencies = {},
28
+ ): Promise<DatabricksClient> {
29
+ const client = new DatabricksClient(
30
+ await createAuthClient(options, undefined, dependencies),
31
+ dependencies.fetch ?? globalThis.fetch,
32
+ );
33
+ logger.debug("created Databricks HTTP client", {
34
+ profile: client.profile(),
35
+ host: client.host(),
36
+ hasWorkspaceId: Boolean(client.workspaceId()),
37
+ });
38
+ return client;
39
+ }
40
+
41
+ profile(): string {
42
+ return this.auth.status().profile;
43
+ }
44
+
45
+ host(): string {
46
+ return this.auth.status().host;
47
+ }
48
+
49
+ principal(): string {
50
+ return this.auth.principal();
51
+ }
52
+
53
+ workspaceId(): string | undefined {
54
+ return this.auth.workspaceId();
55
+ }
56
+
57
+ async request(path: string, options: DatabricksRequestOptions = {}): Promise<unknown> {
58
+ const url = new URL(path, `${this.host().replace(/\/$/, "")}/`).toString();
59
+ const method = options.method ?? (options.body === undefined ? "GET" : "POST");
60
+ const requestUrl = new URL(url);
61
+ logger.debug("sending Databricks API request", {
62
+ method,
63
+ origin: requestUrl.origin,
64
+ path: requestUrl.pathname,
65
+ hasBody: options.body !== undefined,
66
+ login: options.login ?? "auto",
67
+ });
68
+ let authHeaders = await this.auth.authenticate(options.login);
69
+ let response = await this.send(url, method, authHeaders, options);
70
+ logger.debug("received Databricks API response", {
71
+ method,
72
+ path: requestUrl.pathname,
73
+ status: response.status,
74
+ attempt: 1,
75
+ });
76
+ if (response.status === 401) {
77
+ logger.debug("refreshing rejected Databricks API credential", {
78
+ method,
79
+ path: requestUrl.pathname,
80
+ });
81
+ await this.auth.refreshRejectedToken(
82
+ accessTokenFromHeaders(authHeaders),
83
+ options.login ?? true,
84
+ );
85
+ authHeaders = await this.auth.authenticate(false);
86
+ response = await this.send(url, method, authHeaders, options);
87
+ logger.debug("received Databricks API response", {
88
+ method,
89
+ path: requestUrl.pathname,
90
+ status: response.status,
91
+ attempt: 2,
92
+ });
93
+ }
94
+ const text = await response.text();
95
+ if (!response.ok)
96
+ throw new AuthError(
97
+ "http",
98
+ `Databricks API ${path} returned HTTP ${response.status}: ${text}`,
99
+ );
100
+ if (!text) {
101
+ logger.debug("Databricks API response had no body", {
102
+ method,
103
+ path: requestUrl.pathname,
104
+ });
105
+ return undefined;
106
+ }
107
+ try {
108
+ const value = JSON.parse(text);
109
+ logger.debug("parsed Databricks API JSON response", {
110
+ method,
111
+ path: requestUrl.pathname,
112
+ });
113
+ return value;
114
+ } catch (cause) {
115
+ throw new AuthError("http", `Databricks API ${path} did not return JSON`, { cause });
116
+ }
117
+ }
118
+
119
+ private send(
120
+ url: string,
121
+ method: string,
122
+ authHeaders: Record<string, string>,
123
+ options: DatabricksRequestOptions,
124
+ ): Promise<Response> {
125
+ const headers = new Headers(options.headers);
126
+ headers.delete(DEFAULT_ACCESS_TOKEN_HEADER);
127
+ headers.delete(WORKSPACE_ID_HEADER);
128
+ for (const [name, value] of Object.entries(authHeaders)) headers.set(name, value);
129
+ headers.set("accept", "application/json");
130
+ if (options.body !== undefined) headers.set("content-type", "application/json");
131
+ return this.fetcher(url, {
132
+ method,
133
+ headers,
134
+ ...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
135
+ signal: options.signal,
136
+ });
137
+ }
138
+ }
139
+
140
+ function accessTokenFromHeaders(headers: Record<string, string>): string {
141
+ const authorization = headers[DEFAULT_ACCESS_TOKEN_HEADER];
142
+ const separator = authorization?.indexOf(" ") ?? -1;
143
+ if (separator < 1 || !authorization?.slice(separator + 1).trim()) {
144
+ throw new AuthError("http", "Authentication headers did not contain an access token");
145
+ }
146
+ return authorization.slice(separator + 1).trim();
147
+ }