@dbx-tools/auth 0.9.51 → 0.9.58

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.
@@ -0,0 +1,209 @@
1
+ import { RUNTIME_AUTH_TYPE } from "@dbx-tools/shared-auth/client";
2
+ import { TargetKind } from "@dbx-tools/shared-auth/config";
3
+ import { getAccessTokenPayload, getAccessTokenScopes } from "@dbx-tools/shared-core/token";
4
+
5
+ import { AuthError } from "./_errors.ts";
6
+ import { TokenLifecycle } from "./_lifecycle.ts";
7
+ import { normalizeHost } from "./_profile-config.ts";
8
+ import { MemoryCredentialStore } from "./_storage.ts";
9
+ import type { Token, TokenProvider } from "./_types.ts";
10
+ import type { AccessToken, AuthClient, TokenOptions } from "./client.ts";
11
+ import {
12
+ AUTH_DEFAULTS,
13
+ type DatabricksAuthOptions,
14
+ DEFAULT_ACCESS_TOKEN_HEADER,
15
+ WORKSPACE_ID_HEADER,
16
+ } from "./config.ts";
17
+
18
+ const MINIMUM_RUNTIME_TOKEN_TTL_MS = 60_000;
19
+ const RUNTIME_PRINCIPAL = "runtime";
20
+
21
+ /** Python runtime authentication operations backed by one SDK WorkspaceClient. */
22
+ export interface DatabricksRuntimeAuthClient {
23
+ /** Resolved Databricks workspace URL. */
24
+ readonly host: string;
25
+ /** Resolved workspace identifier when the Python SDK provides one. */
26
+ readonly workspaceId?: string;
27
+ /** Resolved caller identifier when the Python SDK provides one. */
28
+ readonly principal?: string;
29
+ /** Return the Python SDK's directly configured token, when present. */
30
+ token(): Promise<string | undefined>;
31
+ /** Return fresh authentication headers from the Python SDK. */
32
+ authenticate(): Promise<Record<string, string>>;
33
+ }
34
+
35
+ interface PythonNodeRuntime {
36
+ databricksRuntimeAuthClient?(): Promise<DatabricksRuntimeAuthClient | undefined>;
37
+ }
38
+
39
+ /**
40
+ * Return auth installed by the shared Python Node runtime, when available.
41
+ */
42
+ export async function _databricksRuntimeAuthClient(): Promise<
43
+ DatabricksRuntimeAuthClient | undefined
44
+ > {
45
+ const runtime = (
46
+ globalThis as typeof globalThis & {
47
+ [key: symbol]: PythonNodeRuntime | undefined;
48
+ }
49
+ )[Symbol.for("@dbx-tools/node-runtime/runtime")];
50
+ return runtime?.databricksRuntimeAuthClient?.();
51
+ }
52
+
53
+ /** Create an auth facade over a Python runtime WorkspaceClient. */
54
+ export function createDatabricksRuntimeAuthClient(
55
+ runtime: DatabricksRuntimeAuthClient,
56
+ options: DatabricksAuthOptions,
57
+ ): AuthClient {
58
+ const lifecycleOptions = {
59
+ ...AUTH_DEFAULTS,
60
+ ...options.auth,
61
+ };
62
+ const provider = new DatabricksRuntimeTokenProvider(runtime, lifecycleOptions.refreshBufferMs);
63
+ const lifecycle = new TokenLifecycle(
64
+ runtimeCacheKey(runtime),
65
+ provider,
66
+ new MemoryCredentialStore(),
67
+ lifecycleOptions,
68
+ );
69
+ return new DefaultDatabricksRuntimeAuthClient(runtime, provider, lifecycle);
70
+ }
71
+
72
+ function runtimeCacheKey(runtime: DatabricksRuntimeAuthClient): string {
73
+ return [
74
+ RUNTIME_AUTH_TYPE,
75
+ normalizeHost(runtime.host, RUNTIME_PRINCIPAL),
76
+ runtime.workspaceId ?? "",
77
+ runtime.principal ?? "",
78
+ ].join(":");
79
+ }
80
+
81
+ function normalizedHeaders(headers: Readonly<Record<string, string>>): Record<string, string> {
82
+ return Object.fromEntries(
83
+ Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]),
84
+ );
85
+ }
86
+
87
+ function runtimeToken(
88
+ headers: Readonly<Record<string, string>>,
89
+ refreshBufferMs: number,
90
+ configuredToken?: string,
91
+ ): Token {
92
+ const authorization = headers[DEFAULT_ACCESS_TOKEN_HEADER];
93
+ const match = authorization?.trim().match(/^(\S+)\s+(.+)$/);
94
+ const accessToken = match?.[2]?.trim() || configuredToken?.trim();
95
+ if (!accessToken) {
96
+ throw new AuthError(
97
+ "oauth",
98
+ "Databricks Python runtime authentication did not provide an access token",
99
+ );
100
+ }
101
+ const payload = getAccessTokenPayload(accessToken);
102
+ const jwtExpiry =
103
+ typeof payload.exp === "number" && Number.isFinite(payload.exp)
104
+ ? new Date(payload.exp * 1000).toISOString()
105
+ : undefined;
106
+ // Opaque runtime credentials still need a bounded Node cache. Reacquire them
107
+ // from the Python SDK after one refresh-buffer interval.
108
+ const expiry =
109
+ jwtExpiry ??
110
+ new Date(
111
+ Date.now() + Math.max(refreshBufferMs * 2, MINIMUM_RUNTIME_TOKEN_TTL_MS),
112
+ ).toISOString();
113
+ return {
114
+ accessToken,
115
+ tokenType: match?.[1] || "Bearer",
116
+ ...(expiry ? { expiry } : {}),
117
+ scopes: [...getAccessTokenScopes(accessToken)],
118
+ };
119
+ }
120
+
121
+ class DatabricksRuntimeTokenProvider implements TokenProvider {
122
+ #accessToken?: string;
123
+ #headers?: Record<string, string>;
124
+
125
+ constructor(
126
+ private readonly runtime: DatabricksRuntimeAuthClient,
127
+ private readonly refreshBufferMs: number,
128
+ ) {}
129
+
130
+ authenticate(): Promise<Token> {
131
+ return this.acquire();
132
+ }
133
+
134
+ login(): Promise<Token> {
135
+ return this.acquire();
136
+ }
137
+
138
+ refresh(): Promise<Token> {
139
+ return this.acquire();
140
+ }
141
+
142
+ canAuthenticateSilently(): boolean {
143
+ return true;
144
+ }
145
+
146
+ headers(token: AccessToken): Record<string, string> {
147
+ if (this.#accessToken === token.accessToken && this.#headers) {
148
+ return { ...this.#headers };
149
+ }
150
+ return {
151
+ [DEFAULT_ACCESS_TOKEN_HEADER]: `${token.tokenType} ${token.accessToken}`,
152
+ };
153
+ }
154
+
155
+ clear(): void {
156
+ this.#accessToken = undefined;
157
+ this.#headers = undefined;
158
+ }
159
+
160
+ private async acquire(): Promise<Token> {
161
+ const headers = normalizedHeaders(await this.runtime.authenticate());
162
+ const token = runtimeToken(headers, this.refreshBufferMs, await this.runtime.token());
163
+ this.#accessToken = token.accessToken;
164
+ this.#headers = {
165
+ ...headers,
166
+ [DEFAULT_ACCESS_TOKEN_HEADER]: `${token.tokenType} ${token.accessToken}`,
167
+ };
168
+ return token;
169
+ }
170
+ }
171
+
172
+ class DefaultDatabricksRuntimeAuthClient implements AuthClient {
173
+ readonly profile = undefined;
174
+ readonly host: string;
175
+ readonly accountId = undefined;
176
+ readonly workspaceId?: string;
177
+ readonly target = TargetKind.Workspace;
178
+ readonly authType = RUNTIME_AUTH_TYPE;
179
+ readonly principal: string;
180
+
181
+ constructor(
182
+ runtime: DatabricksRuntimeAuthClient,
183
+ private readonly provider: DatabricksRuntimeTokenProvider,
184
+ private readonly lifecycle: TokenLifecycle,
185
+ ) {
186
+ this.host = normalizeHost(runtime.host, RUNTIME_PRINCIPAL);
187
+ this.workspaceId = runtime.workspaceId;
188
+ this.principal = runtime.principal || RUNTIME_PRINCIPAL;
189
+ }
190
+
191
+ token(options: TokenOptions = {}): Promise<AccessToken> {
192
+ return options.refresh
193
+ ? this.lifecycle.forceRefresh(options.login ?? true)
194
+ : this.lifecycle.tokenWithLogin(options.login);
195
+ }
196
+
197
+ async headers(options: TokenOptions = {}): Promise<Record<string, string>> {
198
+ const headers = this.provider.headers(await this.token(options));
199
+ if (this.workspaceId && headers[WORKSPACE_ID_HEADER] === undefined) {
200
+ headers[WORKSPACE_ID_HEADER] = this.workspaceId;
201
+ }
202
+ return headers;
203
+ }
204
+
205
+ async logout(): Promise<void> {
206
+ await this.lifecycle.logout();
207
+ this.provider.clear();
208
+ }
209
+ }