@dbx-tools/auth 0.9.32 → 0.9.37
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/README.md +49 -97
- package/index.ts +8 -26
- package/lib/index.d.ts +8 -26
- package/lib/index.js +6 -22
- package/lib/src/{databricks-cli.d.ts → _databricks-cli.d.ts} +3 -7
- package/lib/src/_databricks-cli.js +172 -0
- package/lib/src/_errors.d.ts +5 -0
- package/lib/src/_errors.js +10 -0
- package/lib/src/{lifecycle.d.ts → _lifecycle.d.ts} +5 -5
- package/lib/src/_lifecycle.js +214 -0
- package/lib/src/_logging.d.ts +1 -1
- package/lib/src/_logging.js +2 -2
- package/lib/src/{personal-access-token.d.ts → _personal-access-token.d.ts} +1 -1
- package/lib/src/_personal-access-token.js +30 -0
- package/lib/src/_profile-config.d.ts +43 -0
- package/lib/src/_profile-config.js +443 -0
- package/lib/src/{service-principal.d.ts → _service-principal.d.ts} +1 -1
- package/lib/src/_service-principal.js +69 -0
- package/lib/src/{storage.d.ts → _storage.d.ts} +2 -13
- package/lib/src/_storage.js +94 -0
- package/lib/src/_types.d.ts +40 -0
- package/lib/src/_types.js +2 -0
- package/lib/src/client.d.ts +30 -0
- package/lib/src/client.js +223 -0
- package/lib/src/config.d.ts +36 -0
- package/lib/src/config.js +15 -0
- package/lib/src/profile.d.ts +11 -0
- package/lib/src/profile.js +21 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -10
- package/src/{databricks-cli.ts → _databricks-cli.ts} +20 -109
- package/src/_errors.ts +11 -0
- package/src/{lifecycle.ts → _lifecycle.ts} +12 -40
- package/src/_logging.ts +2 -2
- package/src/{personal-access-token.ts → _personal-access-token.ts} +1 -1
- package/src/_profile-config.ts +545 -0
- package/src/{service-principal.ts → _service-principal.ts} +2 -2
- package/src/{storage.ts → _storage.ts} +2 -31
- package/src/_types.ts +45 -0
- package/src/client.ts +291 -0
- package/src/config.ts +44 -0
- package/src/profile.ts +33 -0
- package/lib/exports.d.ts +0 -2
- package/lib/exports.js +0 -3
- package/lib/src/_profile-core.d.ts +0 -28
- package/lib/src/_profile-core.js +0 -216
- package/lib/src/_profile.d.ts +0 -15
- package/lib/src/_profile.js +0 -236
- package/lib/src/databricks-auth.d.ts +0 -11
- package/lib/src/databricks-auth.js +0 -314
- package/lib/src/databricks-cli.js +0 -239
- package/lib/src/errors.d.ts +0 -7
- package/lib/src/errors.js +0 -14
- package/lib/src/file-storage.d.ts +0 -20
- package/lib/src/file-storage.js +0 -207
- package/lib/src/generated/databricks-cli-assets.json +0 -33
- package/lib/src/http-client.d.ts +0 -23
- package/lib/src/http-client.js +0 -116
- package/lib/src/lifecycle.js +0 -233
- package/lib/src/personal-access-token.js +0 -30
- package/lib/src/service-principal.js +0 -69
- package/lib/src/storage.js +0 -112
- package/lib/src/types.d.ts +0 -165
- package/lib/src/types.js +0 -67
- package/src/_profile-core.ts +0 -258
- package/src/_profile.ts +0 -319
- package/src/databricks-auth.ts +0 -416
- package/src/errors.ts +0 -16
- package/src/file-storage.ts +0 -230
- package/src/generated/databricks-cli-assets.json +0 -33
- package/src/http-client.ts +0 -147
- package/src/types.ts +0 -196
package/src/databricks-auth.ts
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
import * as environmentUtils from "@dbx-tools/shared-core/environment-utils";
|
|
5
|
-
|
|
6
|
-
import { authLogger, tokenMetadata } from "./_logging.ts";
|
|
7
|
-
import { AuthError } from "./errors.ts";
|
|
8
|
-
import { DatabricksCliProvider, resolveDatabricksCli } from "./databricks-cli.ts";
|
|
9
|
-
import { publicToken, TokenLifecycle } from "./lifecycle.ts";
|
|
10
|
-
import { FileCredentialStore } from "./file-storage.ts";
|
|
11
|
-
import { DatabricksPersonalAccessTokenProvider } from "./personal-access-token.ts";
|
|
12
|
-
import {
|
|
13
|
-
listDatabricksProfiles,
|
|
14
|
-
machineScopes,
|
|
15
|
-
resolveConfigFile,
|
|
16
|
-
resolveDatabricksProfile,
|
|
17
|
-
} from "./_profile.ts";
|
|
18
|
-
import { DatabricksServicePrincipalProvider } from "./service-principal.ts";
|
|
19
|
-
import { MemoryCredentialStore } from "./storage.ts";
|
|
20
|
-
import {
|
|
21
|
-
AuthKind,
|
|
22
|
-
type AuthClient,
|
|
23
|
-
AuthOptions,
|
|
24
|
-
type CredentialStore,
|
|
25
|
-
type DatabricksAuthOptions,
|
|
26
|
-
type DatabricksAuthStatus,
|
|
27
|
-
type DatabricksProfile,
|
|
28
|
-
DEFAULT_ACCESS_TOKEN_HEADER,
|
|
29
|
-
Storage,
|
|
30
|
-
TargetKind,
|
|
31
|
-
type Token,
|
|
32
|
-
type TokenProvider,
|
|
33
|
-
WORKSPACE_ID_HEADER,
|
|
34
|
-
} from "./types.ts";
|
|
35
|
-
|
|
36
|
-
const logger = authLogger("databricks");
|
|
37
|
-
|
|
38
|
-
/** Injectable host capabilities for Databricks authentication. */
|
|
39
|
-
export interface DatabricksAuthDependencies {
|
|
40
|
-
environment?: Readonly<Record<string, string | undefined>>;
|
|
41
|
-
fetch?: typeof globalThis.fetch;
|
|
42
|
-
resolveCli?: (options: { install: boolean }) => Promise<string | undefined>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Persistent Databricks auth facade over the provider-neutral lifecycle. */
|
|
46
|
-
class PersistentAuth implements AuthClient {
|
|
47
|
-
constructor(
|
|
48
|
-
private readonly profileValue: DatabricksProfile,
|
|
49
|
-
private readonly storageValue: Storage,
|
|
50
|
-
private readonly options: DatabricksAuthOptions,
|
|
51
|
-
private readonly environment: Readonly<Record<string, string | undefined>>,
|
|
52
|
-
private readonly client?: TokenLifecycle,
|
|
53
|
-
private readonly requestToken?: Token,
|
|
54
|
-
) {}
|
|
55
|
-
|
|
56
|
-
async challenge(): Promise<void> {
|
|
57
|
-
if (!this.client)
|
|
58
|
-
throw new AuthError("oauth", "app_obo uses the current request token and cannot start login");
|
|
59
|
-
logger.debug("authentication challenge requested", this.context());
|
|
60
|
-
await this.client.login();
|
|
61
|
-
logger.debug("authentication challenge completed", this.context());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
token(login?: boolean) {
|
|
65
|
-
logger.debug("Databricks token requested", {
|
|
66
|
-
...this.context(),
|
|
67
|
-
login: login ?? "auto",
|
|
68
|
-
source: this.requestToken ? "request" : "lifecycle",
|
|
69
|
-
});
|
|
70
|
-
return this.requestToken
|
|
71
|
-
? Promise.resolve(publicToken(this.requestToken))
|
|
72
|
-
: this.requiredClient().tokenWithLogin(login);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async authenticate(login?: boolean): Promise<Record<string, string>> {
|
|
76
|
-
const token = await this.token(login);
|
|
77
|
-
const headers = {
|
|
78
|
-
[DEFAULT_ACCESS_TOKEN_HEADER]: `${token.tokenType} ${token.accessToken}`,
|
|
79
|
-
...(this.profileValue.workspaceId
|
|
80
|
-
? { [WORKSPACE_ID_HEADER]: this.profileValue.workspaceId }
|
|
81
|
-
: {}),
|
|
82
|
-
};
|
|
83
|
-
logger.debug("generated Databricks authentication headers", {
|
|
84
|
-
...this.context(),
|
|
85
|
-
headerNames: Object.keys(headers),
|
|
86
|
-
token: tokenMetadata(token),
|
|
87
|
-
});
|
|
88
|
-
return headers;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async authorizationHeaderForUrl(
|
|
92
|
-
requestUrl: string,
|
|
93
|
-
login?: boolean,
|
|
94
|
-
): Promise<string | undefined> {
|
|
95
|
-
return (await this.requestHeadersForUrl(requestUrl, login))[DEFAULT_ACCESS_TOKEN_HEADER];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async requestHeadersForUrl(requestUrl: string, login?: boolean): Promise<Record<string, string>> {
|
|
99
|
-
const request = new URL(requestUrl);
|
|
100
|
-
const credentialOrigin = new URL(this.profileValue.host).origin;
|
|
101
|
-
if (request.origin !== credentialOrigin) {
|
|
102
|
-
logger.debug("skipped authentication for different origin", {
|
|
103
|
-
...this.context(),
|
|
104
|
-
requestOrigin: request.origin,
|
|
105
|
-
credentialOrigin,
|
|
106
|
-
});
|
|
107
|
-
return {};
|
|
108
|
-
}
|
|
109
|
-
logger.debug("applying authentication to matching origin", {
|
|
110
|
-
...this.context(),
|
|
111
|
-
requestOrigin: request.origin,
|
|
112
|
-
});
|
|
113
|
-
return this.authenticate(login);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
forceRefreshToken(login = true) {
|
|
117
|
-
logger.debug("Databricks token force refresh requested", { ...this.context(), login });
|
|
118
|
-
return this.requestToken
|
|
119
|
-
? Promise.resolve(publicToken(this.requestToken))
|
|
120
|
-
: this.requiredClient().forceRefresh(login);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
refreshRejectedToken(staleAccessToken: string, login = true) {
|
|
124
|
-
logger.debug("Databricks rejected token refresh requested", {
|
|
125
|
-
...this.context(),
|
|
126
|
-
login,
|
|
127
|
-
requestToken: Boolean(this.requestToken),
|
|
128
|
-
});
|
|
129
|
-
return this.requestToken
|
|
130
|
-
? Promise.resolve(publicToken(this.requestToken))
|
|
131
|
-
: this.requiredClient().refreshRejectedToken(staleAccessToken, login);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
logout(): Promise<void> {
|
|
135
|
-
logger.debug("Databricks logout requested", this.context());
|
|
136
|
-
return this.client?.logout() ?? Promise.resolve();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
status(): DatabricksAuthStatus {
|
|
140
|
-
return {
|
|
141
|
-
profile: this.profileValue.name,
|
|
142
|
-
host: this.profileValue.host,
|
|
143
|
-
storage: this.storageValue,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
principal(): string {
|
|
148
|
-
return this.profileValue.principal;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
workspaceId(): string | undefined {
|
|
152
|
-
return this.profileValue.workspaceId;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
authKind(): AuthKind {
|
|
156
|
-
return this.profileValue.authKind;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
profile(name?: string) {
|
|
160
|
-
const profile =
|
|
161
|
-
!name || name === this.profileValue.name
|
|
162
|
-
? this.profileValue
|
|
163
|
-
: resolveDatabricksProfile({ ...this.options, profile: name }, this.environment);
|
|
164
|
-
return profileSummary(profile);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
listProfiles(refresh = false) {
|
|
168
|
-
return listDatabricksProfiles(this.options.configFile, refresh, this.environment);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private requiredClient(): TokenLifecycle {
|
|
172
|
-
if (!this.client) throw new AuthError("oauth", "Authentication lifecycle is not available");
|
|
173
|
-
return this.client;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private context(): Record<string, unknown> {
|
|
177
|
-
return {
|
|
178
|
-
profile: this.profileValue.name,
|
|
179
|
-
host: this.profileValue.host,
|
|
180
|
-
authKind: this.profileValue.authKind,
|
|
181
|
-
storage: this.storageValue,
|
|
182
|
-
hasWorkspaceId: Boolean(this.profileValue.workspaceId),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Create a Databricks authentication client. */
|
|
188
|
-
export function createAuthClient(
|
|
189
|
-
options: DatabricksAuthOptions = { preferUserToMachine: true },
|
|
190
|
-
storage = Storage.Auto,
|
|
191
|
-
dependencies: DatabricksAuthDependencies = {},
|
|
192
|
-
): Promise<AuthClient> {
|
|
193
|
-
return createPersistentAuth(options, storage, dependencies);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Resolve a Databricks profile and open built-in credential storage. */
|
|
197
|
-
async function createPersistentAuth(
|
|
198
|
-
options: DatabricksAuthOptions,
|
|
199
|
-
storage: Storage,
|
|
200
|
-
dependencies: DatabricksAuthDependencies,
|
|
201
|
-
): Promise<PersistentAuth> {
|
|
202
|
-
const environment = dependencies.environment ?? process.env;
|
|
203
|
-
const profile = resolveDatabricksProfile(options, environment);
|
|
204
|
-
const inApp =
|
|
205
|
-
profile.authKind === AuthKind.AppOnBehalfOf ||
|
|
206
|
-
profile.authKind === AuthKind.AppServicePrincipal;
|
|
207
|
-
const backend = storage === Storage.Auto ? Storage.Memory : storage;
|
|
208
|
-
logger.debug("creating persistent Databricks auth", {
|
|
209
|
-
profile: profile.name,
|
|
210
|
-
host: profile.host,
|
|
211
|
-
authKind: profile.authKind,
|
|
212
|
-
requestedStorage: storage,
|
|
213
|
-
resolvedStorage: backend,
|
|
214
|
-
inApp,
|
|
215
|
-
});
|
|
216
|
-
const store =
|
|
217
|
-
backend === Storage.Memory
|
|
218
|
-
? new MemoryCredentialStore()
|
|
219
|
-
: new FileCredentialStore(options.cacheDir ?? join(homedir(), ".databricks"));
|
|
220
|
-
return createPersistentAuthWithStorage(options, store, storage, dependencies, profile);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Resolve a Databricks profile using caller-owned generic storage. */
|
|
224
|
-
async function createPersistentAuthWithStorage(
|
|
225
|
-
options: DatabricksAuthOptions,
|
|
226
|
-
store: CredentialStore,
|
|
227
|
-
storage = store.name() === "memory" ? Storage.Memory : Storage.File,
|
|
228
|
-
dependencies: DatabricksAuthDependencies = {},
|
|
229
|
-
resolvedProfile?: DatabricksProfile,
|
|
230
|
-
): Promise<PersistentAuth> {
|
|
231
|
-
const profile =
|
|
232
|
-
resolvedProfile ?? resolveDatabricksProfile(options, dependencies.environment ?? process.env);
|
|
233
|
-
if (profile.authKind === AuthKind.AppOnBehalfOf) {
|
|
234
|
-
logger.debug("created request-scoped App OBO authentication", {
|
|
235
|
-
profile: profile.name,
|
|
236
|
-
host: profile.host,
|
|
237
|
-
hasWorkspaceId: Boolean(profile.workspaceId),
|
|
238
|
-
});
|
|
239
|
-
return new PersistentAuth(
|
|
240
|
-
profile,
|
|
241
|
-
Storage.Memory,
|
|
242
|
-
options,
|
|
243
|
-
dependencies.environment ?? process.env,
|
|
244
|
-
undefined,
|
|
245
|
-
{
|
|
246
|
-
accessToken: profile.accessToken!,
|
|
247
|
-
tokenType: "Bearer",
|
|
248
|
-
scopes: [...profile.scopes],
|
|
249
|
-
},
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
const provider = await providerFor(profile, options, dependencies);
|
|
253
|
-
const client = new TokenLifecycle(
|
|
254
|
-
profile.cacheKey,
|
|
255
|
-
provider,
|
|
256
|
-
store,
|
|
257
|
-
AuthOptions.create(options.auth),
|
|
258
|
-
);
|
|
259
|
-
const persistent = new PersistentAuth(
|
|
260
|
-
profile,
|
|
261
|
-
storage === Storage.Auto ? storageFromName(store.name()) : storage,
|
|
262
|
-
options,
|
|
263
|
-
dependencies.environment ?? process.env,
|
|
264
|
-
client,
|
|
265
|
-
);
|
|
266
|
-
logger.debug("created persistent authentication lifecycle", {
|
|
267
|
-
profile: profile.name,
|
|
268
|
-
host: profile.host,
|
|
269
|
-
authKind: profile.authKind,
|
|
270
|
-
storage: persistent.status().storage,
|
|
271
|
-
});
|
|
272
|
-
return persistent;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function providerFor(
|
|
276
|
-
profile: DatabricksProfile,
|
|
277
|
-
options: DatabricksAuthOptions,
|
|
278
|
-
dependencies: DatabricksAuthDependencies,
|
|
279
|
-
): Promise<TokenProvider> {
|
|
280
|
-
const environment = dependencies.environment ?? process.env;
|
|
281
|
-
const inApp = environmentUtils.isDatabricksAppEnv({ ...environment });
|
|
282
|
-
switch (profile.authKind) {
|
|
283
|
-
case AuthKind.UserToMachine: {
|
|
284
|
-
const install = !inApp || options.installCliInApp === true;
|
|
285
|
-
logger.debug("selected Databricks CLI provider", {
|
|
286
|
-
profile: profile.name,
|
|
287
|
-
inApp,
|
|
288
|
-
install,
|
|
289
|
-
});
|
|
290
|
-
return new DatabricksCliProvider(
|
|
291
|
-
profile.name,
|
|
292
|
-
() =>
|
|
293
|
-
dependencies.resolveCli
|
|
294
|
-
? dependencies.resolveCli({ install })
|
|
295
|
-
: resolveDatabricksCli(environment, { install }),
|
|
296
|
-
cliEnvironment(profile, resolveConfigFile(options.configFile, environment)),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
case AuthKind.PersonalAccessToken:
|
|
300
|
-
logger.debug("selected personal access token provider", { profile: profile.name });
|
|
301
|
-
return new DatabricksPersonalAccessTokenProvider(profile.accessToken!);
|
|
302
|
-
case AuthKind.MachineToMachine:
|
|
303
|
-
case AuthKind.AppServicePrincipal: {
|
|
304
|
-
logger.debug("selected service-principal provider", {
|
|
305
|
-
profile: profile.name,
|
|
306
|
-
authKind: profile.authKind,
|
|
307
|
-
target: profile.target,
|
|
308
|
-
});
|
|
309
|
-
const endpoints = await resolveOAuthEndpoints(profile, dependencies.fetch);
|
|
310
|
-
return new DatabricksServicePrincipalProvider({
|
|
311
|
-
tokenEndpoint: endpoints.tokenEndpoint,
|
|
312
|
-
clientId: profile.clientId,
|
|
313
|
-
clientSecret: profile.clientSecret!,
|
|
314
|
-
scopes: machineScopes(profile.scopes),
|
|
315
|
-
...(profile.groupId ? { groupId: profile.groupId } : {}),
|
|
316
|
-
allowInsecureRequests: isLoopbackHttp(profile.host),
|
|
317
|
-
fetch: dependencies.fetch,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
case AuthKind.AppOnBehalfOf:
|
|
321
|
-
throw new AuthError("config", "App OBO tokens do not use a persistent provider");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
interface AuthorizationServer {
|
|
326
|
-
tokenEndpoint: string;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function resolveOAuthEndpoints(
|
|
330
|
-
profile: DatabricksProfile,
|
|
331
|
-
fetcher: typeof globalThis.fetch = globalThis.fetch,
|
|
332
|
-
): Promise<AuthorizationServer> {
|
|
333
|
-
const host = profile.host.replace(/\/$/, "");
|
|
334
|
-
if (profile.target === TargetKind.Account) {
|
|
335
|
-
if (!profile.accountId) throw new AuthError("config", "Account target requires account_id");
|
|
336
|
-
const endpoints = {
|
|
337
|
-
tokenEndpoint: `${host}/oidc/accounts/${profile.accountId}/v1/token`,
|
|
338
|
-
};
|
|
339
|
-
logger.debug("resolved account OAuth endpoint", {
|
|
340
|
-
profile: profile.name,
|
|
341
|
-
tokenOrigin: new URL(endpoints.tokenEndpoint).origin,
|
|
342
|
-
});
|
|
343
|
-
return endpoints;
|
|
344
|
-
}
|
|
345
|
-
const discovery =
|
|
346
|
-
profile.target === TargetKind.Unified
|
|
347
|
-
? `${host}/oidc/accounts/${requiredAccountId(profile)}/.well-known/oauth-authorization-server`
|
|
348
|
-
: `${host}/oidc/.well-known/oauth-authorization-server`;
|
|
349
|
-
logger.debug("requesting OAuth discovery", {
|
|
350
|
-
profile: profile.name,
|
|
351
|
-
target: profile.target,
|
|
352
|
-
discovery,
|
|
353
|
-
});
|
|
354
|
-
const response = await fetcher(discovery, { redirect: "manual" });
|
|
355
|
-
logger.debug("received OAuth discovery response", {
|
|
356
|
-
profile: profile.name,
|
|
357
|
-
status: response.status,
|
|
358
|
-
});
|
|
359
|
-
if (response.status === 404)
|
|
360
|
-
throw new AuthError("oauth", `OAuth is not supported at ${discovery}`);
|
|
361
|
-
if (!response.ok)
|
|
362
|
-
throw new AuthError("oauth", `OAuth discovery returned HTTP ${response.status}`);
|
|
363
|
-
const value = (await response.json()) as Record<string, unknown>;
|
|
364
|
-
const tokenEndpoint = stringValue(value.token_endpoint);
|
|
365
|
-
if (!tokenEndpoint) throw new AuthError("oauth", "OAuth discovery response is incomplete");
|
|
366
|
-
logger.debug("resolved OAuth token endpoint", {
|
|
367
|
-
profile: profile.name,
|
|
368
|
-
tokenOrigin: new URL(tokenEndpoint).origin,
|
|
369
|
-
});
|
|
370
|
-
return { tokenEndpoint };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function requiredAccountId(profile: DatabricksProfile): string {
|
|
374
|
-
if (!profile.accountId) throw new AuthError("config", "Unified target requires account_id");
|
|
375
|
-
return profile.accountId;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function storageFromName(name: string): Storage {
|
|
379
|
-
return name === "memory" ? Storage.Memory : Storage.File;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function profileSummary(profile: DatabricksProfile) {
|
|
383
|
-
return {
|
|
384
|
-
name: profile.name,
|
|
385
|
-
host: profile.host,
|
|
386
|
-
...(profile.accountId ? { accountId: profile.accountId } : {}),
|
|
387
|
-
...(profile.workspaceId ? { workspaceId: profile.workspaceId } : {}),
|
|
388
|
-
target: profile.target,
|
|
389
|
-
authKind: profile.authKind,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function isLoopbackHttp(host: string): boolean {
|
|
394
|
-
const url = new URL(host);
|
|
395
|
-
return url.protocol === "http:" && ["127.0.0.1", "localhost"].includes(url.hostname);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function cliEnvironment(profile: DatabricksProfile, configFile: string): Record<string, string> {
|
|
399
|
-
return {
|
|
400
|
-
DATABRICKS_CONFIG_FILE: configFile,
|
|
401
|
-
DATABRICKS_CONFIG_PROFILE: profile.name,
|
|
402
|
-
DATABRICKS_HOST: profile.host,
|
|
403
|
-
...(profile.accountId ? { DATABRICKS_ACCOUNT_ID: profile.accountId } : {}),
|
|
404
|
-
...(profile.workspaceId ? { DATABRICKS_WORKSPACE_ID: profile.workspaceId } : {}),
|
|
405
|
-
...(profile.authKind === AuthKind.PersonalAccessToken
|
|
406
|
-
? {
|
|
407
|
-
DATABRICKS_AUTH_TYPE: "pat",
|
|
408
|
-
DATABRICKS_TOKEN: profile.accessToken!,
|
|
409
|
-
}
|
|
410
|
-
: {}),
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function stringValue(value: unknown): string | undefined {
|
|
415
|
-
return typeof value === "string" && value.trim() ? value : undefined;
|
|
416
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
}
|
package/src/file-storage.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { acquireFileLock, type FileLockLease } from "@dbx-tools/core/file-lock";
|
|
7
|
-
|
|
8
|
-
import { authLogger, credentialId, tokenMetadata } from "./_logging.ts";
|
|
9
|
-
import { AuthError } from "./errors.ts";
|
|
10
|
-
import type { CredentialStore, LockAdapter, Token } from "./types.ts";
|
|
11
|
-
import { FileLayout } from "./types.ts";
|
|
12
|
-
|
|
13
|
-
const logger = authLogger("file-storage");
|
|
14
|
-
|
|
15
|
-
interface TokenCache {
|
|
16
|
-
version: number;
|
|
17
|
-
tokens: Record<string, unknown>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class FileLocks implements LockAdapter {
|
|
21
|
-
private readonly leases = new Map<string, FileLockLease>();
|
|
22
|
-
|
|
23
|
-
constructor(private readonly lockDirectory: string) {}
|
|
24
|
-
|
|
25
|
-
async acquire(key: string, timeoutMs?: number): Promise<string> {
|
|
26
|
-
logger.debug("waiting for file lock", {
|
|
27
|
-
credential: credentialId(key),
|
|
28
|
-
timeoutMs,
|
|
29
|
-
});
|
|
30
|
-
const lease = await acquireFileLock(key, { dir: this.lockDirectory, timeoutMs });
|
|
31
|
-
const id = randomUUID();
|
|
32
|
-
this.leases.set(id, lease);
|
|
33
|
-
logger.debug("file lock acquired", {
|
|
34
|
-
credential: credentialId(key),
|
|
35
|
-
backend: lease.backend,
|
|
36
|
-
});
|
|
37
|
-
return id;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async release(id: string): Promise<void> {
|
|
41
|
-
const lease = this.leases.get(id);
|
|
42
|
-
if (!lease) return;
|
|
43
|
-
this.leases.delete(id);
|
|
44
|
-
await lease.release();
|
|
45
|
-
logger.debug("file lock released", { backend: lease.backend });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** File-backed credential store preserving unrelated Databricks CLI entries. */
|
|
50
|
-
export class FileCredentialStore implements CredentialStore {
|
|
51
|
-
private readonly locks: LockAdapter;
|
|
52
|
-
|
|
53
|
-
constructor(
|
|
54
|
-
private readonly root = join(homedir(), ".databricks"),
|
|
55
|
-
private readonly layout = FileLayout.Single,
|
|
56
|
-
locks?: LockAdapter,
|
|
57
|
-
) {
|
|
58
|
-
this.locks = locks ?? new FileLocks(join(root, "locks"));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async load(key: string): Promise<Token | undefined> {
|
|
62
|
-
const store = this.forKey(key);
|
|
63
|
-
return store.withCacheLock(async () => {
|
|
64
|
-
const token = deserializeToken((await store.readCache()).tokens[key]);
|
|
65
|
-
logger.debug("loaded file credential", {
|
|
66
|
-
credential: credentialId(key),
|
|
67
|
-
layout: this.layout,
|
|
68
|
-
token: tokenMetadata(token),
|
|
69
|
-
});
|
|
70
|
-
return token;
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async prepareWrite(): Promise<void> {
|
|
75
|
-
await ensureDirectory(this.root, 0o700);
|
|
76
|
-
logger.debug("prepared credential directory", { layout: this.layout });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async save(key: string, token: Token): Promise<void> {
|
|
80
|
-
const store = this.forKey(key);
|
|
81
|
-
await store.withCacheLock(async () => {
|
|
82
|
-
const cache = await store.readCache();
|
|
83
|
-
cache.tokens[key] = serializeToken(token);
|
|
84
|
-
await store.writeCache(cache);
|
|
85
|
-
logger.debug("saved file credential", {
|
|
86
|
-
credential: credentialId(key),
|
|
87
|
-
layout: this.layout,
|
|
88
|
-
token: tokenMetadata(token),
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async remove(key: string): Promise<void> {
|
|
94
|
-
const store = this.forKey(key);
|
|
95
|
-
await store.withCacheLock(async () => {
|
|
96
|
-
const cache = await store.readCache();
|
|
97
|
-
delete cache.tokens[key];
|
|
98
|
-
await store.writeCache(cache);
|
|
99
|
-
logger.debug("removed file credential", {
|
|
100
|
-
credential: credentialId(key),
|
|
101
|
-
layout: this.layout,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
acquireLock(key: string, timeoutMs?: number): Promise<string> {
|
|
107
|
-
return this.locks.acquire(
|
|
108
|
-
this.layout === FileLayout.Single ? `${this.root}:refresh` : `${this.root}:${key}:refresh`,
|
|
109
|
-
timeoutMs,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
releaseLock(lease: string): Promise<void> {
|
|
114
|
-
return this.locks.release(lease);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
name(): string {
|
|
118
|
-
return "file";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private forKey(key: string): FileCredentialStore {
|
|
122
|
-
if (this.layout === FileLayout.Single) return this;
|
|
123
|
-
const digest = createHash("sha256").update(key).digest("hex");
|
|
124
|
-
return new FileCredentialStore(join(this.root, digest), FileLayout.Single, this.locks);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private async withCacheLock<T>(action: () => Promise<T>): Promise<T> {
|
|
128
|
-
await ensureDirectory(this.root, 0o700);
|
|
129
|
-
const lease = await this.locks.acquire(`${this.root}:cache`, 30_000);
|
|
130
|
-
try {
|
|
131
|
-
return await action();
|
|
132
|
-
} finally {
|
|
133
|
-
await this.locks.release(lease);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private async readCache(): Promise<TokenCache> {
|
|
138
|
-
try {
|
|
139
|
-
const source = await readTextFile(join(this.root, "token-cache.json"));
|
|
140
|
-
const cache = source ? (JSON.parse(source) as TokenCache) : { version: 1, tokens: {} };
|
|
141
|
-
if (cache.version !== 1 || typeof cache.tokens !== "object" || !cache.tokens) {
|
|
142
|
-
throw new AuthError("storage", "Token cache must use version 1");
|
|
143
|
-
}
|
|
144
|
-
logger.debug("read token cache", {
|
|
145
|
-
exists: Boolean(source),
|
|
146
|
-
credentialCount: Object.keys(cache.tokens).length,
|
|
147
|
-
});
|
|
148
|
-
return cache;
|
|
149
|
-
} catch (cause) {
|
|
150
|
-
if (cause instanceof AuthError) throw cause;
|
|
151
|
-
throw new AuthError("storage", "Could not read Databricks token cache", { cause });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async writeCache(cache: TokenCache): Promise<void> {
|
|
156
|
-
try {
|
|
157
|
-
await atomicWriteTextFile(
|
|
158
|
-
join(this.root, "token-cache.json"),
|
|
159
|
-
`${JSON.stringify(cache, null, 2)}\n`,
|
|
160
|
-
0o600,
|
|
161
|
-
);
|
|
162
|
-
logger.debug("wrote token cache", { credentialCount: Object.keys(cache.tokens).length });
|
|
163
|
-
} catch (cause) {
|
|
164
|
-
throw new AuthError("storage", "Could not write Databricks token cache", { cause });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function ensureDirectory(path: string, mode: number): Promise<void> {
|
|
170
|
-
await mkdir(path, { recursive: true, mode });
|
|
171
|
-
await chmod(path, mode);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function readTextFile(path: string): Promise<string | undefined> {
|
|
175
|
-
try {
|
|
176
|
-
return await readFile(path, "utf8");
|
|
177
|
-
} catch (cause) {
|
|
178
|
-
if ((cause as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
179
|
-
throw cause;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function atomicWriteTextFile(path: string, content: string, mode: number): Promise<void> {
|
|
184
|
-
const parent = dirname(path);
|
|
185
|
-
await ensureDirectory(parent, 0o700);
|
|
186
|
-
const temporary = join(parent, `.${basename(path)}-${randomUUID()}.tmp`);
|
|
187
|
-
try {
|
|
188
|
-
await writeFile(temporary, content, { mode, flag: "wx" });
|
|
189
|
-
await chmod(temporary, mode);
|
|
190
|
-
await rename(temporary, path);
|
|
191
|
-
} finally {
|
|
192
|
-
await unlink(temporary).catch((cause: NodeJS.ErrnoException) => {
|
|
193
|
-
if (cause.code !== "ENOENT") throw cause;
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function serializeToken(token: Token): Record<string, unknown> {
|
|
199
|
-
return {
|
|
200
|
-
access_token: token.accessToken,
|
|
201
|
-
token_type: token.tokenType,
|
|
202
|
-
...(token.refreshToken ? { refresh_token: token.refreshToken } : {}),
|
|
203
|
-
...(token.expiry ? { expiry: token.expiry } : {}),
|
|
204
|
-
...(token.scopes.length ? { scopes: token.scopes } : {}),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function deserializeToken(value: unknown): Token | undefined {
|
|
209
|
-
if (!value || typeof value !== "object") return undefined;
|
|
210
|
-
const record = value as Record<string, unknown>;
|
|
211
|
-
const accessToken = stringValue(record.access_token ?? record.accessToken);
|
|
212
|
-
if (!accessToken) return undefined;
|
|
213
|
-
return {
|
|
214
|
-
accessToken,
|
|
215
|
-
tokenType: stringValue(record.token_type ?? record.tokenType) ?? "Bearer",
|
|
216
|
-
...(stringValue(record.refresh_token ?? record.refreshToken)
|
|
217
|
-
? { refreshToken: stringValue(record.refresh_token ?? record.refreshToken) }
|
|
218
|
-
: {}),
|
|
219
|
-
...(stringValue(record.expiry ?? record.expires_at)
|
|
220
|
-
? { expiry: stringValue(record.expiry ?? record.expires_at) }
|
|
221
|
-
: {}),
|
|
222
|
-
scopes: Array.isArray(record.scopes)
|
|
223
|
-
? record.scopes.filter((scope): scope is string => typeof scope === "string")
|
|
224
|
-
: [],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function stringValue(value: unknown): string | undefined {
|
|
229
|
-
return typeof value === "string" && value.trim() ? value : undefined;
|
|
230
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
}
|