@dbx-tools/cli 0.9.54 → 0.9.60

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 (69) hide show
  1. package/README.md +548 -58
  2. package/bin/dbx-graphiti.ts +5 -0
  3. package/bin/dbx-lakebase-proxy.ts +4 -0
  4. package/bin/dbx-model-gateway.ts +5 -0
  5. package/index.ts +35 -1
  6. package/lib/bin/dbx-graphiti.d.ts +2 -0
  7. package/lib/bin/dbx-graphiti.js +4 -0
  8. package/lib/bin/dbx-lakebase-proxy.d.ts +2 -0
  9. package/lib/bin/dbx-lakebase-proxy.js +4 -0
  10. package/lib/bin/dbx-model-gateway.d.ts +2 -0
  11. package/lib/bin/dbx-model-gateway.js +4 -0
  12. package/lib/index.d.ts +35 -1
  13. package/lib/index.js +26 -2
  14. package/lib/src/appkit/cli.d.ts +24 -0
  15. package/lib/src/appkit/cli.js +54 -0
  16. package/lib/src/appkit/env-export.d.ts +40 -0
  17. package/lib/src/appkit/env-export.js +99 -0
  18. package/lib/src/auth/cli.d.ts +20 -0
  19. package/lib/src/auth/cli.js +118 -0
  20. package/lib/src/auth/options.d.ts +41 -0
  21. package/lib/src/auth/options.js +64 -0
  22. package/lib/src/cli.d.ts +2 -0
  23. package/lib/src/cli.js +13 -7
  24. package/lib/src/graphiti/cli.d.ts +18 -0
  25. package/lib/src/graphiti/cli.js +43 -0
  26. package/lib/src/graphiti/options.d.ts +10 -0
  27. package/lib/src/graphiti/options.js +10 -0
  28. package/lib/src/lakebase-proxy/cancellation.d.ts +22 -0
  29. package/lib/src/lakebase-proxy/cancellation.js +100 -0
  30. package/lib/src/lakebase-proxy/cli.d.ts +13 -0
  31. package/lib/src/lakebase-proxy/cli.js +54 -0
  32. package/lib/src/lakebase-proxy/options.d.ts +23 -0
  33. package/lib/src/lakebase-proxy/options.js +41 -0
  34. package/lib/src/lakebase-proxy/protocol.d.ts +37 -0
  35. package/lib/src/lakebase-proxy/protocol.js +124 -0
  36. package/lib/src/lakebase-proxy/proxy.d.ts +22 -0
  37. package/lib/src/lakebase-proxy/proxy.js +200 -0
  38. package/lib/src/model-gateway/cli.d.ts +19 -0
  39. package/lib/src/model-gateway/cli.js +63 -0
  40. package/lib/src/model-gateway/server.d.ts +13 -0
  41. package/lib/src/model-gateway/server.js +53 -0
  42. package/lib/src/tunnel/app.d.ts +20 -0
  43. package/lib/src/tunnel/app.js +52 -0
  44. package/lib/src/tunnel/cli.d.ts +27 -0
  45. package/lib/src/tunnel/cli.js +190 -0
  46. package/lib/src/tunnel/options.d.ts +72 -0
  47. package/lib/src/tunnel/options.js +138 -0
  48. package/lib/src/tunnel/proxy.d.ts +51 -0
  49. package/lib/src/tunnel/proxy.js +143 -0
  50. package/lib/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +124 -9
  52. package/src/appkit/cli.ts +72 -0
  53. package/src/appkit/env-export.ts +124 -0
  54. package/src/auth/cli.ts +165 -0
  55. package/src/auth/options.ts +79 -0
  56. package/src/cli.ts +22 -6
  57. package/src/graphiti/cli.ts +75 -0
  58. package/src/graphiti/options.ts +20 -0
  59. package/src/lakebase-proxy/cancellation.ts +116 -0
  60. package/src/lakebase-proxy/cli.ts +80 -0
  61. package/src/lakebase-proxy/options.ts +56 -0
  62. package/src/lakebase-proxy/protocol.ts +170 -0
  63. package/src/lakebase-proxy/proxy.ts +256 -0
  64. package/src/model-gateway/cli.ts +89 -0
  65. package/src/model-gateway/server.ts +62 -0
  66. package/src/tunnel/app.ts +58 -0
  67. package/src/tunnel/cli.ts +218 -0
  68. package/src/tunnel/options.ts +169 -0
  69. package/src/tunnel/proxy.ts +194 -0
@@ -0,0 +1,170 @@
1
+ /** Minimal PostgreSQL startup protocol helpers for the loopback proxy. */
2
+
3
+ import type { Socket } from "node:net";
4
+
5
+ /** PostgreSQL SSLRequest protocol code. */
6
+ export const SSL_REQUEST = 80_877_103;
7
+ /** PostgreSQL CancelRequest protocol code. */
8
+ export const CANCEL_REQUEST = 80_877_102;
9
+ /** PostgreSQL protocol version 3 code. */
10
+ export const PROTOCOL_3 = 196_608;
11
+
12
+ /** Parsed PostgreSQL startup packet. */
13
+ export interface StartupMessage {
14
+ kind: "startup";
15
+ protocol: number;
16
+ parameters: Record<string, string>;
17
+ }
18
+
19
+ /** Parsed PostgreSQL cancellation packet. */
20
+ export interface CancelMessage {
21
+ kind: "cancel";
22
+ processId: number;
23
+ secretKey: number;
24
+ }
25
+
26
+ /** First supported packet received from a PostgreSQL client. */
27
+ export type InitialMessage = StartupMessage | CancelMessage;
28
+
29
+ /** Error caused by an invalid or unsupported PostgreSQL startup packet. */
30
+ export class PostgresProtocolError extends Error {}
31
+
32
+ /** Read and classify the first PostgreSQL frontend message from a socket. */
33
+ export async function readInitialMessage(
34
+ socket: Socket,
35
+ timeoutMs: number,
36
+ ): Promise<InitialMessage> {
37
+ const deadline = Date.now() + timeoutMs;
38
+ for (;;) {
39
+ const packet = await readPacket(socket, deadline);
40
+ const code = packet.readInt32BE(4);
41
+ if (code === SSL_REQUEST) {
42
+ socket.write("N");
43
+ continue;
44
+ }
45
+ if (code === CANCEL_REQUEST) {
46
+ if (packet.length !== 16) throw new PostgresProtocolError("Invalid CancelRequest packet");
47
+ return {
48
+ kind: "cancel",
49
+ processId: packet.readInt32BE(8),
50
+ secretKey: packet.readInt32BE(12),
51
+ };
52
+ }
53
+ if (code !== PROTOCOL_3) {
54
+ throw new PostgresProtocolError(`Unsupported PostgreSQL protocol version ${code}`);
55
+ }
56
+ return { kind: "startup", protocol: code, parameters: parseParameters(packet.subarray(8)) };
57
+ }
58
+ }
59
+
60
+ /** Build a PostgreSQL SSL negotiation request packet. */
61
+ export function sslRequest(): Buffer {
62
+ return requestPacket(SSL_REQUEST);
63
+ }
64
+
65
+ /** Build a PostgreSQL cancellation request packet. */
66
+ export function cancelRequest(processId: number, secretKey: number): Buffer {
67
+ return requestPacket(CANCEL_REQUEST, processId, secretKey);
68
+ }
69
+
70
+ /** Build the backend messages that complete PostgreSQL startup. */
71
+ export function startupComplete(
72
+ parameters: ReadonlyMap<string, string>,
73
+ processId: number,
74
+ secretKey: number,
75
+ transactionStatus = "I",
76
+ ): Buffer {
77
+ return Buffer.concat([
78
+ backendMessage("R", int32(0)),
79
+ ...[...parameters].map(([name, value]) =>
80
+ backendMessage("S", Buffer.from(`${name}\0${value}\0`)),
81
+ ),
82
+ backendMessage("K", Buffer.concat([int32(processId), int32(secretKey)])),
83
+ backendMessage("Z", Buffer.from(transactionStatus.slice(0, 1) || "I")),
84
+ ]);
85
+ }
86
+
87
+ /** Replace client startup identity with the resolved upstream identity. */
88
+ export function upstreamStartupParameters(
89
+ parameters: Record<string, string>,
90
+ user: string,
91
+ database: string,
92
+ ): Record<string, string> {
93
+ return { ...parameters, user, database };
94
+ }
95
+
96
+ /** Build a fatal PostgreSQL error response. */
97
+ export function fatalError(message: string, sqlstate = "08001"): Buffer {
98
+ return backendMessage(
99
+ "E",
100
+ Buffer.concat([Buffer.from(`SFATAL\0C${sqlstate}\0M${message}\0`), Buffer.from([0])]),
101
+ );
102
+ }
103
+
104
+ function requestPacket(code: number, processId?: number, secretKey?: number): Buffer {
105
+ const values = [code];
106
+ if (processId !== undefined && secretKey !== undefined) values.push(processId, secretKey);
107
+ return Buffer.concat([int32(4 + values.length * 4), ...values.map(int32)]);
108
+ }
109
+
110
+ function backendMessage(code: string, body: Buffer): Buffer {
111
+ return Buffer.concat([Buffer.from(code), int32(body.length + 4), body]);
112
+ }
113
+
114
+ function int32(value: number): Buffer {
115
+ const buffer = Buffer.allocUnsafe(4);
116
+ buffer.writeInt32BE(value);
117
+ return buffer;
118
+ }
119
+
120
+ async function readPacket(socket: Socket, deadline: number): Promise<Buffer> {
121
+ let buffer = Buffer.alloc(0);
122
+ while (buffer.length < 4) buffer = Buffer.concat([buffer, await readChunk(socket, deadline)]);
123
+ const length = buffer.readInt32BE(0);
124
+ if (length < 8 || length > 1024 * 1024) {
125
+ throw new PostgresProtocolError("Invalid PostgreSQL startup packet");
126
+ }
127
+ while (buffer.length < length) {
128
+ buffer = Buffer.concat([buffer, await readChunk(socket, deadline)]);
129
+ }
130
+ const remainder = buffer.subarray(length);
131
+ if (remainder.length) socket.unshift(remainder);
132
+ return buffer.subarray(0, length);
133
+ }
134
+
135
+ function readChunk(socket: Socket, deadline: number): Promise<Buffer> {
136
+ const remaining = deadline - Date.now();
137
+ if (remaining <= 0) {
138
+ return Promise.reject(new PostgresProtocolError("PostgreSQL startup timed out"));
139
+ }
140
+ return new Promise((resolve, reject) => {
141
+ const timer = setTimeout(
142
+ () => finish(new PostgresProtocolError("PostgreSQL startup timed out")),
143
+ remaining,
144
+ );
145
+ const onData = (chunk: Buffer) => finish(undefined, chunk);
146
+ const onError = (error: Error) => finish(error);
147
+ const onEnd = () =>
148
+ finish(new PostgresProtocolError("PostgreSQL client closed during startup"));
149
+ const finish = (error?: Error, chunk?: Buffer) => {
150
+ clearTimeout(timer);
151
+ socket.off("data", onData);
152
+ socket.off("error", onError);
153
+ socket.off("end", onEnd);
154
+ if (error) reject(error);
155
+ else resolve(chunk!);
156
+ };
157
+ socket.once("data", onData);
158
+ socket.once("error", onError);
159
+ socket.once("end", onEnd);
160
+ });
161
+ }
162
+
163
+ function parseParameters(body: Buffer): Record<string, string> {
164
+ const values = body.toString("utf8").split("\0");
165
+ const parameters: Record<string, string> = {};
166
+ for (let index = 0; index + 1 < values.length && values[index]; index += 2) {
167
+ parameters[values[index]!] = values[index + 1] ?? "";
168
+ }
169
+ return parameters;
170
+ }
@@ -0,0 +1,256 @@
1
+ /** Pure Node PostgreSQL wire proxy for Databricks Lakebase. */
2
+
3
+ import { createServer, type Server, type Socket } from "node:net";
4
+ import type { Duplex } from "node:stream";
5
+ import { LakebaseClient, requireAddress } from "@dbx-tools/lakebase";
6
+ import { postgresConnectionOptions } from "@dbx-tools/postgres";
7
+ import { log, stringUtils } from "@dbx-tools/shared-core";
8
+ import { Client } from "pg";
9
+
10
+ import { CancellationRegistry } from "./cancellation.ts";
11
+ import {
12
+ resolveLakebaseProxyOptions,
13
+ type LakebaseProxyOptions,
14
+ type ResolvedLakebaseProxyOptions,
15
+ } from "./options.ts";
16
+ import {
17
+ fatalError,
18
+ PostgresProtocolError,
19
+ readInitialMessage,
20
+ startupComplete,
21
+ upstreamStartupParameters,
22
+ type CancelMessage,
23
+ } from "./protocol.ts";
24
+
25
+ const logger = log.logger("lakebase-proxy");
26
+
27
+ interface PgConnectionInternals {
28
+ stream: Duplex;
29
+ on(event: "parameterStatus", listener: (message: ParameterStatus) => void): void;
30
+ }
31
+
32
+ type PgClientInternals = Client & {
33
+ connection: Client["connection"] & PgConnectionInternals;
34
+ processID: number;
35
+ secretKey: number;
36
+ _txStatus?: string;
37
+ getStartupConf(): Record<string, string>;
38
+ };
39
+
40
+ interface ParameterStatus {
41
+ parameterName: string;
42
+ parameterValue: string;
43
+ }
44
+
45
+ /** Loopback PostgreSQL wire proxy backed by Databricks Lakebase credentials. */
46
+ export class LakebaseProxy {
47
+ private readonly client: LakebaseClient;
48
+ private readonly options: ResolvedLakebaseProxyOptions;
49
+ private readonly cancellations = new CancellationRegistry();
50
+ private server?: Server;
51
+ private statsTimer?: ReturnType<typeof setInterval>;
52
+ private opened = 0;
53
+ private closed = 0;
54
+ private failed = 0;
55
+ private active = 0;
56
+
57
+ constructor(options: LakebaseProxyOptions = {}) {
58
+ this.options = resolveLakebaseProxyOptions(options);
59
+ this.client = new LakebaseClient({ profile: this.options.profile });
60
+ }
61
+
62
+ async listen(): Promise<{ host: string; port: number }> {
63
+ if (this.server) throw new Error("Lakebase proxy is already listening");
64
+ const host = this.options.listen.host;
65
+ const server = createServer((socket) => void this.handle(socket));
66
+ this.server = server;
67
+ this.statsTimer = setInterval(() => this.reportStats(), 60_000);
68
+ this.statsTimer.unref();
69
+ await new Promise<void>((resolve, reject) => {
70
+ server.once("error", reject);
71
+ server.listen(this.options.listen.port, host, () => {
72
+ server.off("error", reject);
73
+ resolve();
74
+ });
75
+ });
76
+ const address = server.address();
77
+ if (!address || typeof address === "string") {
78
+ throw new Error("Lakebase listener has no address");
79
+ }
80
+ logger.info("Lakebase proxy listening", { host: address.address, port: address.port });
81
+ return { host: address.address, port: address.port };
82
+ }
83
+
84
+ async close(): Promise<void> {
85
+ const server = this.server;
86
+ this.server = undefined;
87
+ if (this.statsTimer) clearInterval(this.statsTimer);
88
+ this.statsTimer = undefined;
89
+ if (!server) return;
90
+ await new Promise<void>((resolve, reject) =>
91
+ server.close((error) => (error ? reject(error) : resolve())),
92
+ );
93
+ }
94
+
95
+ private async handle(local: Socket): Promise<void> {
96
+ const started = Date.now();
97
+ let upstream: Duplex | undefined;
98
+ let cancellation: CancelMessage | undefined;
99
+ this.opened += 1;
100
+ this.active += 1;
101
+ try {
102
+ local.setNoDelay(true);
103
+ const initial = await readInitialMessage(local, this.options.startupTimeoutSeconds * 1000);
104
+ if (initial.kind === "cancel") {
105
+ await this.cancellations.forward(initial);
106
+ local.end();
107
+ return;
108
+ }
109
+ const startup = initial;
110
+ const targetText = startup.parameters.database;
111
+ if (!targetText) throw new ProxyFailure("startup database is required", "3D000");
112
+ let target;
113
+ try {
114
+ target = requireAddress(targetText);
115
+ } catch (error) {
116
+ throw new ProxyFailure(message(error), "3D000");
117
+ }
118
+ const startupUser = stringUtils.trimToUndefined(startup.parameters.user);
119
+ let resolved;
120
+ try {
121
+ resolved = await this.client.resolve(target, startupUser);
122
+ } catch (error) {
123
+ throw new ProxyFailure(message(error), "3D000");
124
+ }
125
+ let password;
126
+ try {
127
+ password = await this.client.generateDatabaseCredential(resolved.endpoint, startupUser);
128
+ } catch (error) {
129
+ throw new ProxyFailure(message(error), "28000");
130
+ }
131
+ let connected;
132
+ try {
133
+ connected = await connectUpstream(
134
+ resolved,
135
+ password,
136
+ postgresConnectionOptions(startup.parameters, this.options.postgresRole),
137
+ );
138
+ } catch (error) {
139
+ throw new ProxyFailure(message(error), "08001");
140
+ }
141
+ upstream = connected.socket;
142
+ cancellation = this.cancellations.register({
143
+ host: resolved.host,
144
+ port: resolved.port,
145
+ processId: connected.processId,
146
+ secretKey: connected.secretKey,
147
+ });
148
+ local.write(
149
+ startupComplete(
150
+ connected.parameters,
151
+ cancellation.processId,
152
+ cancellation.secretKey,
153
+ connected.transactionStatus,
154
+ ),
155
+ );
156
+ local.pipe(upstream).pipe(local);
157
+ await Promise.race([closed(local), closed(upstream)]);
158
+ logger.debug("Lakebase connection closed", {
159
+ durationMs: Date.now() - started,
160
+ project: resolved.project,
161
+ branch: resolved.branch,
162
+ endpoint: resolved.endpoint,
163
+ });
164
+ } catch (error) {
165
+ this.failed += 1;
166
+ const failure =
167
+ error instanceof ProxyFailure
168
+ ? error
169
+ : error instanceof PostgresProtocolError
170
+ ? new ProxyFailure(error.message, "08P01")
171
+ : new ProxyFailure(message(error));
172
+ logger.warn("Lakebase connection failed", {
173
+ durationMs: Date.now() - started,
174
+ error: failure.message,
175
+ });
176
+ if (!local.destroyed) local.end(fatalError(failure.message, failure.sqlstate));
177
+ } finally {
178
+ if (cancellation) this.cancellations.remove(cancellation);
179
+ upstream?.destroy();
180
+ local.destroy();
181
+ this.closed += 1;
182
+ this.active -= 1;
183
+ }
184
+ }
185
+
186
+ private reportStats(): void {
187
+ logger.info("Lakebase proxy connections", {
188
+ opened: this.opened,
189
+ closed: this.closed,
190
+ failed: this.failed,
191
+ active: this.active,
192
+ });
193
+ }
194
+ }
195
+
196
+ async function connectUpstream(
197
+ resolved: Awaited<ReturnType<LakebaseClient["resolve"]>>,
198
+ password: string,
199
+ startup: Record<string, string>,
200
+ ): Promise<{
201
+ socket: Duplex;
202
+ parameters: Map<string, string>;
203
+ processId: number;
204
+ secretKey: number;
205
+ transactionStatus: string;
206
+ }> {
207
+ const client = new Client({
208
+ host: resolved.host,
209
+ port: resolved.port,
210
+ database: resolved.database,
211
+ user: resolved.user,
212
+ password,
213
+ ssl: { rejectUnauthorized: true, servername: resolved.host },
214
+ application_name: startup.application_name,
215
+ connectionTimeoutMillis: 30_000,
216
+ }) as unknown as PgClientInternals;
217
+ const parameters = new Map<string, string>();
218
+ client.getStartupConf = () =>
219
+ upstreamStartupParameters(startup, resolved.user, resolved.database);
220
+ client.on("error", (error) => {
221
+ logger.debug("Lakebase upstream client closed", { error: error.message });
222
+ });
223
+ client.connection.on("parameterStatus", (message) => {
224
+ parameters.set(message.parameterName, message.parameterValue);
225
+ });
226
+ await client.connect();
227
+ const socket = client.connection.stream;
228
+ socket.removeAllListeners("data");
229
+ return {
230
+ socket,
231
+ parameters,
232
+ processId: client.processID,
233
+ secretKey: client.secretKey,
234
+ transactionStatus: client._txStatus ?? "I",
235
+ };
236
+ }
237
+
238
+ function closed(socket: Duplex): Promise<void> {
239
+ return new Promise((resolve, reject) => {
240
+ socket.once("close", () => resolve());
241
+ socket.once("error", reject);
242
+ });
243
+ }
244
+
245
+ function message(error: unknown): string {
246
+ return error instanceof Error ? error.message : String(error);
247
+ }
248
+
249
+ class ProxyFailure extends Error {
250
+ constructor(
251
+ message: string,
252
+ readonly sqlstate = "08001",
253
+ ) {
254
+ super(message);
255
+ }
256
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Foreground `dbx model-gateway` command.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { addArgs, parseArgs, serializeArgs } from "@dbx-tools/cli-args";
8
+ import { buildServiceCommand, type CliServiceCliDependencies } from "@dbx-tools/cli-service/cli";
9
+ import { defineService, type CliServiceDefinition } from "@dbx-tools/cli-service/definition";
10
+ import {
11
+ ModelGatewayCliOptionsSchema,
12
+ resolveModelGatewayCliOptions,
13
+ type ModelGatewayCliOptions,
14
+ type ModelGatewayOptions,
15
+ } from "@dbx-tools/shared-model-gateway/options";
16
+ import { Command } from "commander";
17
+
18
+ import { PACKAGE_VERSION } from "../../index.ts";
19
+ import { startModelGateway } from "./server.ts";
20
+
21
+ /** Injectable foreground gateway boundary for CLI tests. */
22
+ export interface ModelGatewayCliDependencies {
23
+ start(options: ModelGatewayOptions): Promise<void>;
24
+ /** Optional service CLI boundary for tests and embedding. */
25
+ readonly service?: CliServiceCliDependencies;
26
+ }
27
+
28
+ const DEFAULT_DEPENDENCIES: ModelGatewayCliDependencies = {
29
+ start: startModelGateway,
30
+ };
31
+
32
+ /** Build the tray-only model-gateway service definition. */
33
+ export function modelGatewayServiceDefinition(
34
+ options: ModelGatewayOptions = {},
35
+ ): CliServiceDefinition {
36
+ const { runtimeInfo: _, ...resolved } = resolveModelGatewayCliOptions(options);
37
+ const host = urlHost(resolved.listen.host);
38
+ return defineService(import.meta.url, {
39
+ id: "dbx-tools.cli-model-gateway",
40
+ name: "dbx model gateway",
41
+ command: {
42
+ binName: "dbx-model-gateway",
43
+ arguments: serializeArgs(resolved),
44
+ },
45
+ menu: [
46
+ {
47
+ type: "url",
48
+ label: "Models",
49
+ url: `http://${host}:${resolved.listen.port}/v1/models`,
50
+ },
51
+ ],
52
+ });
53
+ }
54
+
55
+ /** Build foreground and service model-gateway commands without starting a server. */
56
+ export function buildProgram(
57
+ name = "dbx model-gateway",
58
+ dependencies: ModelGatewayCliDependencies = DEFAULT_DEPENDENCIES,
59
+ ): Command {
60
+ const version = PACKAGE_VERSION;
61
+ const program = addArgs(
62
+ new Command()
63
+ .name(name)
64
+ .description("Run or manage the AppKit Databricks model gateway")
65
+ .version(version, "-v, --version"),
66
+ ModelGatewayCliOptionsSchema,
67
+ ).action(async () => {
68
+ const options = modelGatewayOptions(program);
69
+ if (options.runtimeInfo) {
70
+ process.stdout.write(`${JSON.stringify({ implementation: "typescript-appkit", version })}\n`);
71
+ return;
72
+ }
73
+ const { runtimeInfo: _, ...serverOptions } = options;
74
+ await dependencies.start(serverOptions);
75
+ });
76
+ const serviceCommand = buildServiceCommand(() => {
77
+ return modelGatewayServiceDefinition(modelGatewayOptions(program));
78
+ }, dependencies.service);
79
+ program.addCommand(serviceCommand);
80
+ return program;
81
+ }
82
+
83
+ function modelGatewayOptions(command: Command): ModelGatewayCliOptions {
84
+ return parseArgs(command, ModelGatewayCliOptionsSchema);
85
+ }
86
+
87
+ function urlHost(host: string): string {
88
+ return host.includes(":") ? `[${host}]` : host;
89
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Foreground model-gateway server owned by the CLI package.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { createApp, server } from "@databricks/appkit";
8
+ import { modelGateway } from "@dbx-tools/appkit-model-gateway/plugin";
9
+ import { sendHealth } from "@dbx-tools/appkit-model-gateway/routes";
10
+ import { workspaceClient } from "@dbx-tools/databricks";
11
+ import {
12
+ resolveModelGatewayOptions,
13
+ type ModelGatewayOptions,
14
+ type ResolvedModelGatewayOptions,
15
+ } from "@dbx-tools/shared-model-gateway/options";
16
+
17
+ type AppKitServerOptions = NonNullable<Parameters<typeof server>[0]>;
18
+
19
+ /** Resolve AppKit server settings for the loopback model gateway. */
20
+ export function modelGatewayServerOptions(options: ModelGatewayOptions = {}): AppKitServerOptions {
21
+ return appKitServerOptions(resolveModelGatewayOptions(options));
22
+ }
23
+
24
+ function appKitServerOptions(options: ResolvedModelGatewayOptions): AppKitServerOptions {
25
+ return {
26
+ bodyLimit: options.bodyLimit,
27
+ host: options.listen.host,
28
+ port: options.listen.port,
29
+ };
30
+ }
31
+
32
+ /** Start the foreground model gateway and keep its AppKit server active. */
33
+ export async function startModelGateway(options: ModelGatewayOptions = {}): Promise<void> {
34
+ const resolved = resolveModelGatewayOptions(options);
35
+ const client = await workspaceClient.createWorkspaceClient({
36
+ ...(resolved.profile ? { profile: resolved.profile } : {}),
37
+ });
38
+ await createApp({
39
+ client,
40
+ plugins: [modelGateway(), server(appKitServerOptions(resolved))],
41
+ onPluginsReady(appkit) {
42
+ appkit.server.extend((application) => {
43
+ application.get("/api/healthz", (_request, response) => {
44
+ sendHealth(response);
45
+ });
46
+ application.get("/v1/models", (request, response) => {
47
+ void appkit.modelGateway.models(request, response);
48
+ });
49
+ for (const [path, protocol] of [
50
+ ["/v1/chat/completions", "openai-chat"],
51
+ ["/v1/responses", "openai-responses"],
52
+ ["/v1/messages", "anthropic-messages"],
53
+ ["/v1/embeddings", "openai-embeddings"],
54
+ ] as const) {
55
+ application.post(path, (request, response) => {
56
+ void appkit.modelGateway.inference(protocol, request, response);
57
+ });
58
+ }
59
+ });
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The GATE half of the wrapper: a server-less AppKit app whose only job is to
3
+ * expose the `authGate` handlers the proxy calls.
4
+ *
5
+ * The in-process plugin path (`authGate` in the app's own `plugins`) has a real
6
+ * HTTP server to mount routes on. A wrapper does not - the app it fronts is a
7
+ * separate process it must not reach into - so the gate's login routes live on
8
+ * the proxy instead, and this app exists purely to give the plugin the runtime it
9
+ * needs: a `CacheManager` for the one-time-code store and signing key, and the
10
+ * sibling `email` plugin's transport for delivering a code.
11
+ *
12
+ * Lazily imported by `cli.ts`, so `dbx tunnel --insecure` (and `install` /
13
+ * `status`) never load AppKit, the Databricks SDK, or the SMTP stack.
14
+ *
15
+ * @module
16
+ */
17
+
18
+ import { lakebase, type WorkspaceClient } from "@databricks/appkit";
19
+ import { appkit } from "@dbx-tools/appkit";
20
+ import { authStorage } from "@dbx-tools/auth-gate";
21
+ import { email } from "@dbx-tools/email";
22
+ import { postgresConnectionOptions } from "@dbx-tools/postgres";
23
+ import { object } from "@dbx-tools/shared-core";
24
+ import { authGate, type AuthGateApi, type AuthGateConfig } from "@dbx-tools/tunnel";
25
+
26
+ /** Boot the gate app and return the handlers the proxy authenticates against. */
27
+ export async function startGateApp(
28
+ config: AuthGateConfig,
29
+ client?: WorkspaceClient,
30
+ ): Promise<AuthGateApi> {
31
+ // No `server()` plugin: nothing here listens. `plugins` order matters - `email`
32
+ // registers its transport before `authGate` looks for a sibling to send with.
33
+ const plugins = await appkit.createApp({
34
+ client,
35
+ plugins: [
36
+ ...(authStorage.shouldUseLakebase(config)
37
+ ? [lakebase({ pool: postgresConnectionOptions({}) })]
38
+ : []),
39
+ ...(config.sendCode ? [] : [email()]),
40
+ authGate(config),
41
+ ],
42
+ });
43
+ const api = Reflect.get(plugins, "authGate");
44
+ if (!isAuthGateApi(api)) throw new Error("the authGate plugin exposed no api");
45
+ return api;
46
+ }
47
+
48
+ function isAuthGateApi(value: unknown): value is AuthGateApi {
49
+ return (
50
+ object.isRecord(value) &&
51
+ typeof value.basePath === "string" &&
52
+ typeof value.passkeysEnabled === "boolean" &&
53
+ typeof value.handler === "function" &&
54
+ typeof value.session === "function" &&
55
+ typeof value.status === "function" &&
56
+ typeof value.close === "function"
57
+ );
58
+ }