Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/appkit/src/connectors/mlflow/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,9 @@ async function resolveViaSdk(
// Mints the OAuth access token (or reuses a PAT from the profile) and adds
// an `Authorization: Bearer <token>` header — the same call the connectors
// use before each request.
await client.config.authenticate(headers);
await client.authenticate(headers);
const token = options.token ?? extractBearer(headers);
const host =
options.host ??
(await client.config.getHost()).toString().replace(/\/+$/, "");
const host = options.host ?? (await client.getHost());
if (!token || !host) return undefined;
return { host, token };
} catch {
Expand Down
38 changes: 38 additions & 0 deletions packages/appkit/src/connectors/mlflow/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, test, vi } from "vitest";

const { createWorkspaceClient } = vi.hoisted(() => ({
createWorkspaceClient: vi.fn(),
}));
vi.mock("../../../workspace-client", () => ({ createWorkspaceClient }));

import { resolveDatabricksAuth } from "../auth";

describe("resolveDatabricksAuth", () => {
test("profile path: takes the bearer + host from the client's modular auth seam", async () => {
createWorkspaceClient.mockReturnValue({
authenticate: async (headers: Headers) => {
headers.set("Authorization", "Bearer minted");
},
getHost: async () => "https://ws.cloud.databricks.com",
});

await expect(
resolveDatabricksAuth({ profile: "dogfood" }),
).resolves.toEqual({
host: "https://ws.cloud.databricks.com",
token: "minted",
});
expect(createWorkspaceClient).toHaveBeenCalledWith({ profile: "dogfood" });
});

test("returns undefined when credentials can't be resolved", async () => {
createWorkspaceClient.mockReturnValue({
authenticate: async () => {
throw new Error("no auth configured");
},
getHost: async () => "https://x",
});

await expect(resolveDatabricksAuth({})).resolves.toBeUndefined();
});
});
14 changes: 6 additions & 8 deletions packages/appkit/src/context/service-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,20 +226,18 @@ export class ServiceContext {
return process.env.DATABRICKS_WORKSPACE_ID;
}

const response = (await client.apiClient.request({
const response = await client.request({
path: "/api/2.0/preview/scim/v2/Me",
method: "GET",
headers: new Headers(),
raw: false,
query: {},
responseHeaders: ["x-databricks-org-id"],
})) as { "x-databricks-org-id": string };
});
await response.body?.cancel();
const workspaceId = response.headers.get("x-databricks-org-id");

if (!response["x-databricks-org-id"]) {
if (!workspaceId) {
throw ConfigurationError.resourceNotFound("Workspace ID");
}

return response["x-databricks-org-id"];
return workspaceId;
}

/**
Expand Down
11 changes: 9 additions & 2 deletions packages/appkit/src/context/tests/service-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ const { mockMe, mockApiRequest, MockWorkspaceClient, MockConfigError } =

const MockWorkspaceClient = vi.fn().mockImplementation(() => ({
currentUser: { me: mockMe },
apiClient: { request: mockApiRequest },
// Tests script legacy-style results; adapt them to the raw `Response`
// `client.request` returns (org id → response header, else JSON body).
request: async (req: unknown) => {
const result = await mockApiRequest(req);
const orgId = result?.["x-databricks-org-id"];
return orgId !== undefined
? new Response(null, { headers: { "x-databricks-org-id": orgId } })
: new Response(JSON.stringify(result ?? {}));
},
}));

class MockConfigError extends Error {
Expand Down Expand Up @@ -411,7 +419,6 @@ describe("ServiceContext", () => {
expect.objectContaining({
path: "/api/2.0/preview/scim/v2/Me",
method: "GET",
responseHeaders: ["x-databricks-org-id"],
}),
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ import { createApp } from "../appkit";
const { MockWorkspaceClient } = vi.hoisted(() => {
const MockWorkspaceClient = vi.fn().mockImplementation(() => ({
currentUser: { me: vi.fn().mockResolvedValue({ id: "sp-user-123" }) },
apiClient: {
request: vi.fn().mockResolvedValue({ "x-databricks-org-id": "ws-456" }),
},
request: vi
.fn()
.mockResolvedValue(
new Response(null, { headers: { "x-databricks-org-id": "ws-456" } }),
),
}));
return { MockWorkspaceClient };
});
Expand Down
8 changes: 4 additions & 4 deletions packages/appkit/src/internal-telemetry/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ export class TelemetryReporter {
async #send(logs: AppkitLog[]): Promise<void> {
if (logs.length === 0) return;
const workspaceId = await this.#workspaceIdPromise;
await this.#client.apiClient.request({
const response = await this.#client.request({
path: "/telemetry-ext",
method: "POST",
query: { o: workspaceId },
headers: new Headers(),
payload: buildAppkitPayload(logs),
raw: false,
headers: new Headers({ "Content-Type": "application/json" }),
body: JSON.stringify(buildAppkitPayload(logs)),
});
await response.body?.cancel();
}
}
13 changes: 7 additions & 6 deletions packages/appkit/src/internal-telemetry/tests/reporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ function createMockClient(): {
client: WorkspaceClient;
request: RequestSpy;
} {
const request = vi.fn().mockResolvedValue({});
const client = { apiClient: { request } } as unknown as WorkspaceClient;
const request = vi.fn(async () => new Response("{}"));
const client = { request } as unknown as WorkspaceClient;
return { client, request };
}

Expand Down Expand Up @@ -43,7 +43,7 @@ afterEach(() => {
function lastProtoLog(spy: RequestSpy, callIndex = -1) {
const calls = spy.mock.calls;
const idx = callIndex < 0 ? calls.length + callIndex : callIndex;
const payload = calls[idx][0].payload as { protoLogs: string[] };
const payload = JSON.parse(calls[idx][0].body) as { protoLogs: string[] };
return JSON.parse(payload.protoLogs[0]);
}

Expand All @@ -52,7 +52,7 @@ describe("TelemetryReporter", () => {
expect(TelemetryReporter.getInstance()).toBeNull();
});

test("sendStartup emits an APP_STARTUP appkit_log via apiClient.request", async () => {
test("sendStartup emits an APP_STARTUP appkit_log via client.request", async () => {
const opts = baseOpts();
const reporter = TelemetryReporter.initialize(opts);
await reporter.sendStartup();
Expand All @@ -63,8 +63,8 @@ describe("TelemetryReporter", () => {
path: "/telemetry-ext",
method: "POST",
query: { o: "1234567890" },
raw: false,
});
expect(reqArg.headers.get("Content-Type")).toBe("application/json");
expect(lastProtoLog(opts.__spy).entry.appkit_log).toMatchObject({
event_name: "APP_STARTUP",
app_id: "app-uuid-1",
Expand Down Expand Up @@ -95,7 +95,8 @@ describe("TelemetryReporter", () => {
await reporter.flushRequestMetrics();

expect(opts.__spy).toHaveBeenCalledOnce();
const protoLogs = opts.__spy.mock.calls[0][0].payload.protoLogs as string[];
const protoLogs = JSON.parse(opts.__spy.mock.calls[0][0].body)
.protoLogs as string[];
expect(protoLogs).toHaveLength(2);

const events = protoLogs
Expand Down
5 changes: 2 additions & 3 deletions packages/appkit/src/plugins/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,11 +864,10 @@ export class AgentsPlugin extends Plugin implements ToolProvider {
try {
const { getWorkspaceClient } = await import("../../context");
const wsClient = getWorkspaceClient();
await wsClient.config.ensureResolved();
host = wsClient.config.host;
host = await wsClient.getHost();
authenticate = async () => {
const headers = new Headers();
await wsClient.config.authenticate(headers);
await wsClient.authenticate(headers);
return Object.fromEntries(headers.entries());
};
} catch {
Expand Down
12 changes: 7 additions & 5 deletions packages/appkit/src/resources/tests/warehouse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe("warehouse resource bindings", () => {
const client = createMockWorkspaceClient();
const bindings = await WarehouseResource.resolve(client, true);
expect(await bindings.warehouseId).toBe("configured-warehouse");
expect(client.apiClient.request).not.toHaveBeenCalled();
expect(client.request).not.toHaveBeenCalled();
expect(getWarehouseId).toThrow(InitializationError);
WarehouseResource.bind(bindings);
expect(getWarehouseId()).toBe(bindings.warehouseId);
Expand All @@ -78,7 +78,7 @@ describe("warehouse resource bindings", () => {
const client = createMockWorkspaceClient();
const bindings = await WarehouseResource.resolve(client);
expect(bindings.warehouseId).toBeUndefined();
expect(client.apiClient.request).not.toHaveBeenCalled();
expect(client.request).not.toHaveBeenCalled();
WarehouseResource.bind(bindings);
expect(getWarehouseId).toThrow(ConfigurationError);
expect(getWarehouseId).toThrow("No plugin requires a SQL Warehouse");
Expand All @@ -95,7 +95,7 @@ describe("warehouse resource bindings", () => {
);
WarehouseResource.bind(binding);
expect(await getWarehouseId()).toBe("plugin-warehouse");
expect(client.apiClient.request).not.toHaveBeenCalled();
expect(client.request).not.toHaveBeenCalled();
});

test("shares one ID across declarations with different environment variables", () => {
Expand Down Expand Up @@ -174,13 +174,15 @@ describe("warehouse resource bindings", () => {
vi.stubEnv("DATABRICKS_WAREHOUSE_ID", "");
vi.stubEnv("DATABRICKS_APPS_AGENTIC_MODE", "");
const client = createMockWorkspaceClient();
vi.mocked(client.apiClient.request).mockResolvedValue(response);
vi.mocked(client.request).mockResolvedValue(
new Response(JSON.stringify(response)),
);
const error = await WarehouseResource.resolve(client, true).catch(
(e: unknown) => e,
);
expect(error).toBeInstanceOf(ConfigurationError);
expect(error).not.toBeInstanceOf(TypeError);
expect(client.apiClient.request).toHaveBeenCalledTimes(1);
expect(client.request).toHaveBeenCalledTimes(1);
},
);
});
14 changes: 7 additions & 7 deletions packages/appkit/src/resources/warehouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ async function discoverWarehouseId(client: WorkspaceClient): Promise<string> {
process.env.DATABRICKS_APPS_AGENTIC_MODE === "1";

if (process.env.NODE_ENV === "development" && !agenticMode) {
const response = (await client.apiClient.request({
path: "/api/2.0/sql/warehouses",
method: "GET",
headers: new Headers(),
raw: false,
query: { skip_cannot_use: "true" },
})) as { warehouses: sql.EndpointInfo[] };
const response = (await (
await client.request({
path: "/api/2.0/sql/warehouses",
method: "GET",
query: { skip_cannot_use: "true" },
})
).json()) as { warehouses?: sql.EndpointInfo[] };

const priorities: Record<sql.State, number> = {
RUNNING: 0,
Expand Down
19 changes: 19 additions & 0 deletions packages/appkit/src/testing/mock-workspace-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,26 @@ export function createMockWorkspaceClient(
return legacy;
}

// Modular auth seam. Seed via `responses: { request: ... }` (a `Response`, or
// a function returning one); `request` defaults to a fresh `{}` JSON body.
const seamDefaults: Record<string, (...args: Any[]) => Any> = {
getHost: async () => configTarget.host,
authenticate: async (headers: Headers) => {
headers.set("Authorization", "Bearer test-token");
},
request: async () => new Response("{}"),
};
const seam: Pick<WorkspaceClient, "getHost" | "authenticate" | "request"> =
Object.fromEntries(
Object.entries(seamDefaults).map(([key, impl]) => {
const fn = key in merged ? mint(key) : vi.fn(impl);
fns.set(key, fn);
return [key, fn];
}),
) as Any;

const client: WorkspaceClient = {
...seam,
...(Object.fromEntries(
FACADE_SERVICES.map((name) => [name, service(name)]),
) as Pick<WorkspaceClient, (typeof FACADE_SERVICES)[number]>),
Expand Down
24 changes: 24 additions & 0 deletions packages/shared/src/workspace-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import {
import {
buildStatementExecutionClient,
buildWarehousesClient,
buildWorkspaceAuth,
type StatementExecutionClient,
type WarehousesClient,
type WorkspaceAuth,
type WorkspaceRequest,
} from "./modular";
import type { WorkspaceClient } from "./types";

Expand All @@ -25,6 +28,7 @@ export class AppKitWorkspaceClient implements WorkspaceClient {
#legacy?: LegacyWorkspaceClient;
#warehouses?: WarehousesClient;
#statementExecution?: StatementExecutionClient;
#auth?: WorkspaceAuth;

constructor(opts: WorkspaceClientOptions) {
this.#opts = opts;
Expand Down Expand Up @@ -66,6 +70,19 @@ export class AppKitWorkspaceClient implements WorkspaceClient {
return this.#getLegacy().currentUser;
}

// Modular auth + raw-request seam — built lazily, independent of the legacy client.
getHost(): Promise<string> {
return this.#getAuth().getHost();
}

authenticate(headers: Headers): Promise<void> {
return this.#getAuth().authenticate(headers);
}

request(req: WorkspaceRequest): Promise<Response> {
return this.#getAuth().request(req);
}

get config() {
return this.#getLegacy().config;
}
Expand All @@ -78,6 +95,13 @@ export class AppKitWorkspaceClient implements WorkspaceClient {
return this.#getLegacy();
}

#getAuth(): WorkspaceAuth {
if (!this.#auth) {
this.#auth = buildWorkspaceAuth(this.#opts);
}
return this.#auth;
}

#getLegacy(): LegacyWorkspaceClient {
if (!this.#legacy) {
this.#legacy = buildLegacyWorkspaceClient(this.#opts);
Expand Down
Loading
Loading