@dbx-tools/auth 0.9.31 → 0.9.36

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 (73) hide show
  1. package/README.md +49 -97
  2. package/index.ts +8 -26
  3. package/lib/index.d.ts +8 -26
  4. package/lib/index.js +6 -22
  5. package/lib/src/{databricks-cli.d.ts → _databricks-cli.d.ts} +3 -7
  6. package/lib/src/_databricks-cli.js +172 -0
  7. package/lib/src/_errors.d.ts +5 -0
  8. package/lib/src/_errors.js +10 -0
  9. package/lib/src/{lifecycle.d.ts → _lifecycle.d.ts} +5 -5
  10. package/lib/src/_lifecycle.js +214 -0
  11. package/lib/src/_logging.d.ts +1 -1
  12. package/lib/src/_logging.js +2 -2
  13. package/lib/src/{personal-access-token.d.ts → _personal-access-token.d.ts} +1 -1
  14. package/lib/src/_personal-access-token.js +30 -0
  15. package/lib/src/_profile-config.d.ts +43 -0
  16. package/lib/src/_profile-config.js +443 -0
  17. package/lib/src/{service-principal.d.ts → _service-principal.d.ts} +1 -1
  18. package/lib/src/_service-principal.js +69 -0
  19. package/lib/src/{storage.d.ts → _storage.d.ts} +4 -6
  20. package/lib/src/_storage.js +94 -0
  21. package/lib/src/_types.d.ts +40 -0
  22. package/lib/src/_types.js +2 -0
  23. package/lib/src/client.d.ts +30 -0
  24. package/lib/src/client.js +223 -0
  25. package/lib/src/config.d.ts +36 -0
  26. package/lib/src/config.js +15 -0
  27. package/lib/src/profile.d.ts +11 -0
  28. package/lib/src/profile.js +21 -0
  29. package/lib/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +34 -10
  31. package/src/{databricks-cli.ts → _databricks-cli.ts} +20 -109
  32. package/src/_errors.ts +11 -0
  33. package/src/{lifecycle.ts → _lifecycle.ts} +24 -48
  34. package/src/_logging.ts +2 -2
  35. package/src/{personal-access-token.ts → _personal-access-token.ts} +1 -1
  36. package/src/_profile-config.ts +545 -0
  37. package/src/{service-principal.ts → _service-principal.ts} +2 -2
  38. package/src/_storage.ts +109 -0
  39. package/src/_types.ts +45 -0
  40. package/src/client.ts +291 -0
  41. package/src/config.ts +44 -0
  42. package/src/profile.ts +33 -0
  43. package/lib/exports.d.ts +0 -2
  44. package/lib/exports.js +0 -3
  45. package/lib/src/_profile-core.d.ts +0 -28
  46. package/lib/src/_profile-core.js +0 -216
  47. package/lib/src/_profile.d.ts +0 -15
  48. package/lib/src/_profile.js +0 -236
  49. package/lib/src/databricks-auth.d.ts +0 -11
  50. package/lib/src/databricks-auth.js +0 -314
  51. package/lib/src/databricks-cli.js +0 -239
  52. package/lib/src/errors.d.ts +0 -7
  53. package/lib/src/errors.js +0 -14
  54. package/lib/src/generated/databricks-cli-assets.json +0 -33
  55. package/lib/src/http-client.d.ts +0 -23
  56. package/lib/src/http-client.js +0 -116
  57. package/lib/src/lifecycle.js +0 -229
  58. package/lib/src/node-storage.d.ts +0 -20
  59. package/lib/src/node-storage.js +0 -207
  60. package/lib/src/personal-access-token.js +0 -30
  61. package/lib/src/service-principal.js +0 -69
  62. package/lib/src/storage.js +0 -98
  63. package/lib/src/types.d.ts +0 -157
  64. package/lib/src/types.js +0 -67
  65. package/src/_profile-core.ts +0 -258
  66. package/src/_profile.ts +0 -319
  67. package/src/databricks-auth.ts +0 -416
  68. package/src/errors.ts +0 -16
  69. package/src/generated/databricks-cli-assets.json +0 -33
  70. package/src/http-client.ts +0 -147
  71. package/src/node-storage.ts +0 -230
  72. package/src/storage.ts +0 -104
  73. package/src/types.ts +0 -188
@@ -1,258 +0,0 @@
1
- import { parse as parseIni } from "ini";
2
-
3
- import { authLogger } from "./_logging.ts";
4
- import { AuthError } from "./errors.ts";
5
- import {
6
- AUTH_TYPE_APP_OBO,
7
- AUTH_TYPE_APP_SP,
8
- AuthKind,
9
- DEFAULT_ACCESS_TOKEN_HEADER,
10
- TargetKind,
11
- } from "./types.ts";
12
-
13
- const logger = authLogger("profile-selection");
14
-
15
- export const SETTINGS_SECTION = "__settings__";
16
-
17
- export type Environment = Readonly<Record<string, string | undefined>>;
18
- export type IniConfig = Map<string, Map<string, string>>;
19
-
20
- export interface RawProfile {
21
- host?: string;
22
- accountId?: string;
23
- workspaceId?: string;
24
- clientId?: string;
25
- clientSecret?: string;
26
- accessToken?: string;
27
- groupId?: string;
28
- scopes?: string;
29
- authType?: string;
30
- }
31
-
32
- /** Parse Databricks INI text through the maintained `ini` package. */
33
- export function parseDatabricksConfig(source: string): IniConfig {
34
- let parsed: Record<string, unknown>;
35
- try {
36
- parsed = parseIni(source) as Record<string, unknown>;
37
- } catch (cause) {
38
- throw new AuthError("config", "Databricks configuration is not valid INI", { cause });
39
- }
40
- const config: IniConfig = new Map();
41
- for (const [name, value] of Object.entries(parsed)) {
42
- if (!name.trim() || !isRecord(value))
43
- throw new AuthError("config", "Databricks configuration values must belong to a profile");
44
- const section = new Map<string, string>();
45
- for (const [key, entry] of Object.entries(value)) {
46
- if (entry === undefined || entry === null) continue;
47
- section.set(key.trim().toLowerCase(), String(entry).trim());
48
- }
49
- config.set(name.trim(), section);
50
- }
51
- return config;
52
- }
53
-
54
- export function loadRawProfile(config: IniConfig | undefined, name: string): RawProfile {
55
- const section = config?.get(name);
56
- return {
57
- host: section?.get("host"),
58
- accountId: section?.get("account_id"),
59
- workspaceId: section?.get("workspace_id"),
60
- clientId: section?.get("client_id"),
61
- clientSecret: section?.get("client_secret"),
62
- accessToken: section?.get("token"),
63
- groupId: section?.get("group_id"),
64
- scopes: section?.get("scopes"),
65
- authType: nonempty(section?.get("auth_type"))?.toLowerCase(),
66
- };
67
- }
68
-
69
- export function resolveProfileName(
70
- requested: string | undefined,
71
- explicit: boolean,
72
- config: IniConfig | undefined,
73
- preferUserToMachine: boolean,
74
- ): string {
75
- let selected = requested;
76
- let source = requested ? "requested" : undefined;
77
- if (!selected) {
78
- selected = nonempty(config?.get(SETTINGS_SECTION)?.get("default_profile"));
79
- if (selected) source = "settings-default";
80
- }
81
- if (!selected && config?.has("DEFAULT")) {
82
- selected = "DEFAULT";
83
- source = "default-section";
84
- }
85
- if (!selected) {
86
- const profiles = [...(config?.keys() ?? [])].filter((name) => name !== SETTINGS_SECTION);
87
- if (profiles.length === 1) {
88
- selected = profiles[0];
89
- source = "sole-profile";
90
- }
91
- }
92
- if (!selected) {
93
- selected = "DEFAULT";
94
- source = "fallback";
95
- }
96
- if (selected === SETTINGS_SECTION)
97
- throw new AuthError("config", `${SETTINGS_SECTION} is reserved`);
98
- if (explicit || !preferUserToMachine || !config) {
99
- logger.debug("selected Databricks profile", {
100
- profile: selected,
101
- source,
102
- explicit,
103
- preferUserToMachine,
104
- });
105
- return selected;
106
- }
107
- const current = loadRawProfile(config, selected);
108
- if (!isM2mProfile(current) || !current.host) {
109
- logger.debug("selected Databricks profile", {
110
- profile: selected,
111
- source,
112
- explicit,
113
- preferUserToMachine,
114
- });
115
- return selected;
116
- }
117
- const matches = [...config.keys()].filter((name) => {
118
- if (name === selected || name === SETTINGS_SECTION) return false;
119
- const candidate = loadRawProfile(config, name);
120
- return candidate.authType === "databricks-cli" && sameTarget(current, candidate);
121
- });
122
- const preferred = matches.length === 1 ? matches[0]! : selected;
123
- logger.debug("selected Databricks profile", {
124
- profile: preferred,
125
- source: preferred === selected ? source : "matching-cli-profile",
126
- originalProfile: preferred === selected ? undefined : selected,
127
- explicit,
128
- preferUserToMachine,
129
- matchingCliProfiles: matches.length,
130
- });
131
- return preferred;
132
- }
133
-
134
- /** Normalize Databricks hosts and require TLS outside loopback development. */
135
- export function normalizeHost(value: string | undefined, profile = "DEFAULT"): string {
136
- const selected = nonempty(value);
137
- if (!selected) throw new AuthError("config", `Profile ${profile} has no host`);
138
- const withScheme = /^https?:\/\//i.test(selected) ? selected : `https://${selected}`;
139
- let url: URL;
140
- try {
141
- url = new URL(withScheme);
142
- } catch (cause) {
143
- throw new AuthError("config", `Profile ${profile} has an invalid host`, { cause });
144
- }
145
- if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
146
- throw new AuthError("config", "Databricks host must use HTTPS");
147
- }
148
- return url.toString().replace(/\/$/, "");
149
- }
150
-
151
- export function resolveAuthKind(
152
- authType: string | undefined,
153
- clientId: string | undefined,
154
- clientSecret: string | undefined,
155
- accessToken: string | undefined,
156
- ): AuthKind {
157
- switch (authType) {
158
- case "databricks-cli":
159
- case "oauth-u2m":
160
- return AuthKind.UserToMachine;
161
- case "oauth-m2m":
162
- if (!clientId || !clientSecret)
163
- throw new AuthError("config", "oauth-m2m requires client_id and client_secret");
164
- return AuthKind.MachineToMachine;
165
- case "pat":
166
- if (!accessToken) throw new AuthError("config", "pat requires token");
167
- return AuthKind.PersonalAccessToken;
168
- case AUTH_TYPE_APP_OBO:
169
- case "app-obo":
170
- if (!accessToken)
171
- throw new AuthError("config", "app_obo requires the configured access token header");
172
- return AuthKind.AppOnBehalfOf;
173
- case AUTH_TYPE_APP_SP:
174
- case "app-sp":
175
- if (!clientId || !clientSecret)
176
- throw new AuthError("config", "app_sp requires client id and secret");
177
- return AuthKind.AppServicePrincipal;
178
- case undefined:
179
- if (clientId && clientSecret) return AuthKind.MachineToMachine;
180
- if (clientSecret) throw new AuthError("config", "oauth-m2m client_secret requires client_id");
181
- return accessToken ? AuthKind.PersonalAccessToken : AuthKind.UserToMachine;
182
- default:
183
- throw new AuthError("config", `Authentication type ${authType} is not supported`);
184
- }
185
- }
186
-
187
- export function requestOboToken(
188
- headers?: Record<string, string>,
189
- headerName = DEFAULT_ACCESS_TOKEN_HEADER,
190
- ): string | undefined {
191
- const entry = Object.entries(headers ?? {}).find(
192
- ([name]) => name.toLowerCase() === headerName.toLowerCase(),
193
- );
194
- const value = nonempty(entry?.[1]);
195
- if (!value) return undefined;
196
- if (headerName.toLowerCase() !== DEFAULT_ACCESS_TOKEN_HEADER) return value;
197
- const [scheme, ...parts] = value.split(/\s+/);
198
- return scheme?.toLowerCase() === "bearer" ? nonempty(parts.join(" ")) : undefined;
199
- }
200
-
201
- export function inferTarget(host: string | undefined, accountId: string | undefined): TargetKind {
202
- if (accountId && host) {
203
- try {
204
- if (new URL(normalizeHost(host)).hostname === "accounts.cloud.databricks.com")
205
- return TargetKind.Account;
206
- } catch {}
207
- }
208
- return TargetKind.Workspace;
209
- }
210
-
211
- export function parseTarget(value: string): TargetKind {
212
- switch (value.trim().toLowerCase()) {
213
- case TargetKind.Workspace:
214
- return TargetKind.Workspace;
215
- case TargetKind.Account:
216
- return TargetKind.Account;
217
- case TargetKind.Unified:
218
- return TargetKind.Unified;
219
- default:
220
- throw new AuthError("config", "Target must be workspace, account, or unified");
221
- }
222
- }
223
-
224
- export function cleanList(values: readonly string[]): string[] {
225
- return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
226
- }
227
-
228
- export function nonempty(value: string | undefined): string | undefined {
229
- const selected = value?.trim();
230
- return selected ? selected : undefined;
231
- }
232
-
233
- export function missing(profile: string, field: string): never {
234
- throw new AuthError("config", `Profile ${profile} requires ${field}`);
235
- }
236
-
237
- function sameTarget(left: RawProfile, right: RawProfile): boolean {
238
- try {
239
- if (normalizeHost(left.host) !== normalizeHost(right.host)) return false;
240
- } catch {
241
- return false;
242
- }
243
- return (
244
- (!left.accountId || left.accountId === right.accountId) &&
245
- (!left.workspaceId || left.workspaceId === right.workspaceId)
246
- );
247
- }
248
-
249
- function isM2mProfile(profile: RawProfile): boolean {
250
- return (
251
- profile.authType === "oauth-m2m" ||
252
- (!profile.authType && Boolean(profile.clientId && profile.clientSecret))
253
- );
254
- }
255
-
256
- function isRecord(value: unknown): value is Record<string, unknown> {
257
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
258
- }
package/src/_profile.ts DELETED
@@ -1,319 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { isAbsolute, resolve } from "node:path";
5
-
6
- import * as environmentUtils from "@dbx-tools/shared-core/environment-utils";
7
-
8
- import { authLogger } from "./_logging.ts";
9
- import {
10
- cleanList,
11
- type Environment,
12
- inferTarget,
13
- type IniConfig,
14
- loadRawProfile,
15
- missing,
16
- nonempty,
17
- normalizeHost,
18
- parseDatabricksConfig,
19
- parseTarget,
20
- requestOboToken,
21
- resolveAuthKind,
22
- resolveProfileName,
23
- SETTINGS_SECTION,
24
- } from "./_profile-core.ts";
25
- import { AuthError } from "./errors.ts";
26
- import {
27
- AUTH_TYPE_APP_OBO,
28
- AUTH_TYPE_APP_SP,
29
- AuthKind,
30
- type DatabricksAuthOptions,
31
- type DatabricksProfile,
32
- type DatabricksProfileSummary,
33
- DEFAULT_CLIENT_ID,
34
- DEFAULT_CONFIG_FILE,
35
- } from "./types.ts";
36
-
37
- const configCache = new Map<string, IniConfig | Error | undefined>();
38
- const logger = authLogger("profile");
39
-
40
- export { normalizeHost, parseDatabricksConfig } from "./_profile-core.ts";
41
-
42
- /** Expand and resolve the selected Databricks CLI configuration path. */
43
- export function resolveConfigFile(
44
- explicit?: string,
45
- environment: Environment = process.env,
46
- ): string {
47
- const selected =
48
- nonempty(explicit) ?? nonempty(environment.DATABRICKS_CONFIG_FILE) ?? DEFAULT_CONFIG_FILE;
49
- const expanded =
50
- selected === "~"
51
- ? homedir()
52
- : selected.startsWith("~/")
53
- ? resolve(homedir(), selected.slice(2))
54
- : selected;
55
- const path = isAbsolute(expanded) ? expanded : resolve(expanded);
56
- logger.debug("resolved Databricks config file", {
57
- path,
58
- source: nonempty(explicit)
59
- ? "option"
60
- : nonempty(environment.DATABRICKS_CONFIG_FILE)
61
- ? "environment"
62
- : "default",
63
- });
64
- return path;
65
- }
66
-
67
- /** Invalidate the process-wide parsed configuration cache after an external write. */
68
- export function invalidateConfigFile(configFile?: string): void {
69
- const path = resolveConfigFile(configFile);
70
- configCache.delete(path);
71
- logger.debug("invalidated Databricks config cache", { path });
72
- }
73
-
74
- /** Return whether a named profile exists in the selected configuration. */
75
- export function configProfileExists(profile: string, configFile?: string): boolean {
76
- if (!profile.trim() || profile === SETTINGS_SECTION) return false;
77
- return loadConfig(resolveConfigFile(configFile))?.has(profile) ?? false;
78
- }
79
-
80
- /** Enumerate secret-free profile metadata from the cached CLI configuration. */
81
- export function listDatabricksProfiles(
82
- configFile?: string,
83
- refresh = false,
84
- environment: Environment = process.env,
85
- ): DatabricksProfileSummary[] {
86
- const path = resolveConfigFile(configFile, environment);
87
- if (refresh) configCache.delete(path);
88
- const config = loadConfig(path);
89
- if (!config) {
90
- logger.debug("listed Databricks profiles", { path, refresh, count: 0 });
91
- return [];
92
- }
93
- const profiles = [...config.keys()]
94
- .filter((name) => name !== SETTINGS_SECTION)
95
- .map((name) => {
96
- const profile = loadRawProfile(config, name);
97
- const host = nonempty(profile.host);
98
- const accountId = nonempty(profile.accountId);
99
- return {
100
- name,
101
- ...(host ? { host } : {}),
102
- ...(accountId ? { accountId } : {}),
103
- ...(nonempty(profile.workspaceId) ? { workspaceId: nonempty(profile.workspaceId) } : {}),
104
- target: inferTarget(host, accountId),
105
- authKind: resolveAuthKind(
106
- profile.authType,
107
- profile.clientId,
108
- profile.clientSecret,
109
- profile.accessToken,
110
- ),
111
- };
112
- })
113
- .sort((left, right) => left.name.localeCompare(right.name));
114
- logger.debug("listed Databricks profiles", { path, refresh, count: profiles.length });
115
- return profiles;
116
- }
117
-
118
- /** Resolve options, environment, request headers, and CLI configuration into one profile. */
119
- export function resolveDatabricksProfile(
120
- options: DatabricksAuthOptions,
121
- environment: Environment = process.env,
122
- ): DatabricksProfile {
123
- const inApp = environmentUtils.isDatabricksAppEnv({ ...environment });
124
- const environmentProfile = nonempty(environment.DATABRICKS_CONFIG_PROFILE);
125
- const explicitProfile = Boolean(nonempty(options.profile) ?? environmentProfile);
126
- const requestToken = requestOboToken(options.requestHeaders, options.accessTokenHeader);
127
- const explicitAuthType =
128
- nonempty(options.authType)?.toLowerCase() ??
129
- (!inApp && !explicitProfile
130
- ? nonempty(environment.DATABRICKS_AUTH_TYPE)?.toLowerCase()
131
- : undefined);
132
- const appServicePrincipal = [
133
- environment.DATABRICKS_HOST,
134
- environment.DATABRICKS_CLIENT_ID,
135
- environment.DATABRICKS_CLIENT_SECRET,
136
- ].every(nonempty);
137
- const selectedAuthType =
138
- !inApp || explicitProfile || explicitAuthType
139
- ? explicitAuthType
140
- : requestToken
141
- ? AUTH_TYPE_APP_OBO
142
- : appServicePrincipal
143
- ? AUTH_TYPE_APP_SP
144
- : undefined;
145
- const appAuth = [AUTH_TYPE_APP_OBO, "app-obo", AUTH_TYPE_APP_SP, "app-sp"].includes(
146
- selectedAuthType ?? "",
147
- );
148
- const ignoreAmbientCredentials = explicitProfile && !appAuth;
149
- const configPath = resolveConfigFile(options.configFile, environment);
150
- const config = loadConfig(configPath);
151
- const requestedName = nonempty(options.profile) ?? environmentProfile;
152
- const profileName = resolveProfileName(
153
- requestedName,
154
- explicitProfile,
155
- config,
156
- options.preferUserToMachine,
157
- );
158
- const configured = loadRawProfile(config, profileName);
159
- const ambient = (name: keyof NodeJS.ProcessEnv): string | undefined =>
160
- ignoreAmbientCredentials ? undefined : nonempty(environment[name]);
161
- const host = normalizeHost(
162
- options.host ?? ambient("DATABRICKS_HOST") ?? configured.host,
163
- profileName,
164
- );
165
- const accountId =
166
- nonempty(options.accountId) ??
167
- ambient("DATABRICKS_ACCOUNT_ID") ??
168
- nonempty(configured.accountId);
169
- const workspaceId =
170
- nonempty(options.workspaceId) ??
171
- ambient("DATABRICKS_WORKSPACE_ID") ??
172
- nonempty(configured.workspaceId);
173
- const clientIdValue =
174
- nonempty(options.clientId) ?? ambient("DATABRICKS_CLIENT_ID") ?? nonempty(configured.clientId);
175
- const clientSecret =
176
- nonempty(options.clientSecret) ??
177
- ambient("DATABRICKS_CLIENT_SECRET") ??
178
- nonempty(configured.clientSecret);
179
- const accessToken =
180
- selectedAuthType === AUTH_TYPE_APP_OBO || selectedAuthType === "app-obo"
181
- ? requestToken
182
- : (nonempty(options.accessToken) ??
183
- ambient("DATABRICKS_TOKEN") ??
184
- nonempty(configured.accessToken));
185
- const authType =
186
- selectedAuthType ??
187
- ambient("DATABRICKS_AUTH_TYPE") ??
188
- nonempty(configured.authType)?.toLowerCase();
189
- const authKind = resolveAuthKind(authType, clientIdValue, clientSecret, accessToken);
190
- const clientId =
191
- authKind === AuthKind.UserToMachine
192
- ? (clientIdValue ?? DEFAULT_CLIENT_ID)
193
- : authKind === AuthKind.MachineToMachine || authKind === AuthKind.AppServicePrincipal
194
- ? (clientIdValue ?? missing(profileName, "client_id"))
195
- : (clientIdValue ?? "");
196
- const scopes = options.scopes?.length
197
- ? cleanList(options.scopes)
198
- : cleanList(configured.scopes?.split(",") ?? ["all-apis"]);
199
- const target = options.target ? parseTarget(options.target) : inferTarget(host, accountId);
200
- const groupId =
201
- nonempty(options.groupId) ?? ambient("DATABRICKS_GROUP_ID") ?? nonempty(configured.groupId);
202
- const principal =
203
- authKind === AuthKind.MachineToMachine || authKind === AuthKind.AppServicePrincipal
204
- ? clientId
205
- : profileName;
206
- const cacheKey = credentialCacheKey({
207
- name: profileName,
208
- host,
209
- accountId,
210
- workspaceId,
211
- clientId,
212
- groupId,
213
- scopes,
214
- authKind,
215
- accessToken,
216
- });
217
- logger.debug("resolved Databricks profile", {
218
- profile: profileName,
219
- host,
220
- authKind,
221
- target,
222
- inApp,
223
- explicitProfile,
224
- authType: authType ?? "automatic",
225
- hasAccountId: Boolean(accountId),
226
- hasWorkspaceId: Boolean(workspaceId),
227
- hasGroupId: Boolean(groupId),
228
- scopeCount: scopes.length,
229
- configPath,
230
- });
231
- return {
232
- name: profileName,
233
- host,
234
- ...(accountId ? { accountId } : {}),
235
- ...(workspaceId ? { workspaceId } : {}),
236
- clientId,
237
- ...(groupId ? { groupId } : {}),
238
- scopes,
239
- target,
240
- authKind,
241
- ...(authType ? { authType } : {}),
242
- ...(clientSecret ? { clientSecret } : {}),
243
- ...(accessToken ? { accessToken } : {}),
244
- cacheKey,
245
- principal,
246
- };
247
- }
248
-
249
- function loadConfig(path: string): IniConfig | undefined {
250
- if (configCache.has(path)) {
251
- const cached = configCache.get(path);
252
- if (cached instanceof Error) throw cached;
253
- logger.debug("reused Databricks config cache", {
254
- path,
255
- exists: Boolean(cached),
256
- profileCount: cached?.size ?? 0,
257
- });
258
- return cached;
259
- }
260
- if (!existsSync(path)) {
261
- configCache.set(path, undefined);
262
- logger.debug("Databricks config file not found", { path });
263
- return undefined;
264
- }
265
- try {
266
- const config = parseDatabricksConfig(readFileSync(path, "utf8"));
267
- configCache.set(path, config);
268
- logger.debug("parsed Databricks config file", { path, profileCount: config.size });
269
- return config;
270
- } catch (cause) {
271
- const error =
272
- cause instanceof Error ? cause : new AuthError("config", `Could not read ${path}`, { cause });
273
- configCache.set(path, error);
274
- throw error;
275
- }
276
- }
277
-
278
- function credentialCacheKey(
279
- profile: Pick<
280
- DatabricksProfile,
281
- | "name"
282
- | "host"
283
- | "accountId"
284
- | "workspaceId"
285
- | "clientId"
286
- | "groupId"
287
- | "scopes"
288
- | "authKind"
289
- | "accessToken"
290
- >,
291
- ): string {
292
- if (profile.authKind === AuthKind.UserToMachine) return profile.name;
293
- if (profile.authKind === AuthKind.PersonalAccessToken) {
294
- const digest = createHash("sha256")
295
- .update(profile.accessToken ?? "")
296
- .digest("hex");
297
- return `${profile.name}-pat-${digest}`;
298
- }
299
- if (profile.authKind === AuthKind.AppOnBehalfOf) return `${profile.name}-app-obo`;
300
- const identity = [
301
- profile.host,
302
- profile.accountId ?? "",
303
- profile.workspaceId ?? "",
304
- profile.clientId,
305
- profile.groupId ?? "",
306
- machineScopes(profile.scopes).join(" "),
307
- ].join("\0");
308
- const digest = createHash("sha256").update(identity).digest("hex");
309
- return `${profile.name}-${profile.authKind === AuthKind.AppServicePrincipal ? "app-sp" : "oauth-m2m"}-${digest}`;
310
- }
311
-
312
- export function effectiveScopes(scopes: readonly string[]): string[] {
313
- return cleanList(["offline_access", ...scopes]);
314
- }
315
-
316
- export function machineScopes(scopes: readonly string[]): string[] {
317
- const values = cleanList(scopes.length ? scopes : ["all-apis"]);
318
- return [...values].sort();
319
- }