@dbx-tools/auth 0.6.182 → 0.9.29

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,416 @@
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 "./node-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
+ }