@dbx-tools/cli 0.9.58 → 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.
- package/README.md +548 -58
- package/bin/dbx-graphiti.ts +5 -0
- package/bin/dbx-lakebase-proxy.ts +4 -0
- package/bin/dbx-model-gateway.ts +5 -0
- package/index.ts +35 -1
- package/lib/bin/dbx-graphiti.d.ts +2 -0
- package/lib/bin/dbx-graphiti.js +4 -0
- package/lib/bin/dbx-lakebase-proxy.d.ts +2 -0
- package/lib/bin/dbx-lakebase-proxy.js +4 -0
- package/lib/bin/dbx-model-gateway.d.ts +2 -0
- package/lib/bin/dbx-model-gateway.js +4 -0
- package/lib/index.d.ts +35 -1
- package/lib/index.js +26 -2
- package/lib/src/appkit/cli.d.ts +24 -0
- package/lib/src/appkit/cli.js +54 -0
- package/lib/src/appkit/env-export.d.ts +40 -0
- package/lib/src/appkit/env-export.js +99 -0
- package/lib/src/auth/cli.d.ts +20 -0
- package/lib/src/auth/cli.js +118 -0
- package/lib/src/auth/options.d.ts +41 -0
- package/lib/src/auth/options.js +64 -0
- package/lib/src/cli.d.ts +2 -0
- package/lib/src/cli.js +13 -7
- package/lib/src/graphiti/cli.d.ts +18 -0
- package/lib/src/graphiti/cli.js +43 -0
- package/lib/src/graphiti/options.d.ts +10 -0
- package/lib/src/graphiti/options.js +10 -0
- package/lib/src/lakebase-proxy/cancellation.d.ts +22 -0
- package/lib/src/lakebase-proxy/cancellation.js +100 -0
- package/lib/src/lakebase-proxy/cli.d.ts +13 -0
- package/lib/src/lakebase-proxy/cli.js +54 -0
- package/lib/src/lakebase-proxy/options.d.ts +23 -0
- package/lib/src/lakebase-proxy/options.js +41 -0
- package/lib/src/lakebase-proxy/protocol.d.ts +37 -0
- package/lib/src/lakebase-proxy/protocol.js +124 -0
- package/lib/src/lakebase-proxy/proxy.d.ts +22 -0
- package/lib/src/lakebase-proxy/proxy.js +200 -0
- package/lib/src/model-gateway/cli.d.ts +19 -0
- package/lib/src/model-gateway/cli.js +63 -0
- package/lib/src/model-gateway/server.d.ts +13 -0
- package/lib/src/model-gateway/server.js +53 -0
- package/lib/src/tunnel/app.d.ts +20 -0
- package/lib/src/tunnel/app.js +52 -0
- package/lib/src/tunnel/cli.d.ts +27 -0
- package/lib/src/tunnel/cli.js +190 -0
- package/lib/src/tunnel/options.d.ts +72 -0
- package/lib/src/tunnel/options.js +138 -0
- package/lib/src/tunnel/proxy.d.ts +51 -0
- package/lib/src/tunnel/proxy.js +143 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +124 -9
- package/src/appkit/cli.ts +72 -0
- package/src/appkit/env-export.ts +124 -0
- package/src/auth/cli.ts +165 -0
- package/src/auth/options.ts +79 -0
- package/src/cli.ts +22 -6
- package/src/graphiti/cli.ts +75 -0
- package/src/graphiti/options.ts +20 -0
- package/src/lakebase-proxy/cancellation.ts +116 -0
- package/src/lakebase-proxy/cli.ts +80 -0
- package/src/lakebase-proxy/options.ts +56 -0
- package/src/lakebase-proxy/protocol.ts +170 -0
- package/src/lakebase-proxy/proxy.ts +256 -0
- package/src/model-gateway/cli.ts +89 -0
- package/src/model-gateway/server.ts +62 -0
- package/src/tunnel/app.ts +58 -0
- package/src/tunnel/cli.ts +218 -0
- package/src/tunnel/options.ts +169 -0
- package/src/tunnel/proxy.ts +194 -0
package/src/auth/cli.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dbx auth` Commander program for Databricks OAuth.
|
|
3
|
+
*
|
|
4
|
+
* The command delegates profile resolution and token lifecycle to
|
|
5
|
+
* `@dbx-tools/auth`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as databricks from "@dbx-tools/auth";
|
|
11
|
+
import { addArgs, parseArgs } from "@dbx-tools/cli-args";
|
|
12
|
+
import type { DatabricksAuthClientInfo } from "@dbx-tools/shared-auth/client";
|
|
13
|
+
import { Command, CommanderError } from "commander";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
AuthCliOptionsSchema,
|
|
17
|
+
databricksAuthOptions,
|
|
18
|
+
TokenCommandOptionsSchema,
|
|
19
|
+
type AuthCliOptions,
|
|
20
|
+
} from "./options.ts";
|
|
21
|
+
|
|
22
|
+
interface AuthContext {
|
|
23
|
+
auth: databricks.AuthClient;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AuthCliDependencies {
|
|
28
|
+
createAuthClient: typeof databricks.client.createAuthClient;
|
|
29
|
+
writeJson(value: unknown): void;
|
|
30
|
+
writeText(value: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_DEPENDENCIES: AuthCliDependencies = {
|
|
34
|
+
createAuthClient: databricks.client.createAuthClient,
|
|
35
|
+
writeJson: (value) => {
|
|
36
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
37
|
+
},
|
|
38
|
+
writeText: (value) => {
|
|
39
|
+
process.stdout.write(`${value}\n`);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Open the selected authentication client. */
|
|
44
|
+
async function openAuth(
|
|
45
|
+
options: AuthCliOptions,
|
|
46
|
+
dependencies: AuthCliDependencies,
|
|
47
|
+
): Promise<AuthContext> {
|
|
48
|
+
return {
|
|
49
|
+
auth: await dependencies.createAuthClient(databricksAuthOptions(options)),
|
|
50
|
+
close: async () => {},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Execute an auth action and close resources owned by the command. */
|
|
55
|
+
async function withAuth(
|
|
56
|
+
options: AuthCliOptions,
|
|
57
|
+
dependencies: AuthCliDependencies,
|
|
58
|
+
action: (context: AuthContext) => Promise<void>,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const context = await openAuth(options, dependencies);
|
|
61
|
+
try {
|
|
62
|
+
await action(context);
|
|
63
|
+
} finally {
|
|
64
|
+
await context.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Shape a generated token record for stable CLI JSON output. */
|
|
69
|
+
function tokenJson(token: databricks.AccessToken): Record<string, unknown> {
|
|
70
|
+
return {
|
|
71
|
+
access_token: token.accessToken,
|
|
72
|
+
token_type: token.tokenType,
|
|
73
|
+
...(token.expiry ? { expiry: token.expiry } : {}),
|
|
74
|
+
...(token.scopes.length ? { scopes: token.scopes } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Select the serializable identity fields from an authentication client. */
|
|
79
|
+
function clientInfo(auth: databricks.AuthClient): DatabricksAuthClientInfo {
|
|
80
|
+
return {
|
|
81
|
+
...(auth.profile ? { profile: auth.profile } : {}),
|
|
82
|
+
host: auth.host,
|
|
83
|
+
...(auth.accountId ? { accountId: auth.accountId } : {}),
|
|
84
|
+
...(auth.workspaceId ? { workspaceId: auth.workspaceId } : {}),
|
|
85
|
+
target: auth.target,
|
|
86
|
+
authType: auth.authType,
|
|
87
|
+
principal: auth.principal,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Build the `dbx auth` Commander program without parsing arguments. */
|
|
92
|
+
export function buildProgram(
|
|
93
|
+
name = "dbx auth",
|
|
94
|
+
overrides: Partial<AuthCliDependencies> = {},
|
|
95
|
+
): Command {
|
|
96
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
97
|
+
const program = addArgs(
|
|
98
|
+
new Command()
|
|
99
|
+
.name(name)
|
|
100
|
+
.description("Authenticate to Databricks with user or machine OAuth")
|
|
101
|
+
.showHelpAfterError(),
|
|
102
|
+
AuthCliOptionsSchema,
|
|
103
|
+
);
|
|
104
|
+
const options = (): AuthCliOptions => parseArgs(program, AuthCliOptionsSchema);
|
|
105
|
+
|
|
106
|
+
program
|
|
107
|
+
.command("login")
|
|
108
|
+
.description("Force browser login and return an access token")
|
|
109
|
+
.action(async () => {
|
|
110
|
+
await withAuth(options(), dependencies, async ({ auth }) => {
|
|
111
|
+
dependencies.writeJson(tokenJson(await auth.token({ login: true })));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const tokenCommand = addArgs(
|
|
116
|
+
program.command("token").description("Return a valid access token, logging in when needed"),
|
|
117
|
+
TokenCommandOptionsSchema,
|
|
118
|
+
);
|
|
119
|
+
tokenCommand.action(async () => {
|
|
120
|
+
const tokenOptions = parseArgs(tokenCommand, TokenCommandOptionsSchema);
|
|
121
|
+
await withAuth(options(), dependencies, async ({ auth }) => {
|
|
122
|
+
const token = await auth.token({
|
|
123
|
+
...(tokenOptions.login ? {} : { login: false }),
|
|
124
|
+
...(tokenOptions.forceRefresh ? { refresh: true } : {}),
|
|
125
|
+
});
|
|
126
|
+
dependencies.writeJson(tokenJson(token));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
program
|
|
131
|
+
.command("profile")
|
|
132
|
+
.description("Print the configured or automatically detected profile")
|
|
133
|
+
.action(async () => {
|
|
134
|
+
await withAuth(options(), dependencies, async ({ auth }) => {
|
|
135
|
+
dependencies.writeText(auth.profile ?? "ambient");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
program
|
|
140
|
+
.command("logout")
|
|
141
|
+
.description("Delete the stored credential for the selected profile")
|
|
142
|
+
.action(async () => {
|
|
143
|
+
await withAuth(options(), dependencies, async ({ auth }) => {
|
|
144
|
+
await auth.logout();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
program
|
|
149
|
+
.command("status")
|
|
150
|
+
.description("Show the resolved authentication client configuration")
|
|
151
|
+
.action(async () => {
|
|
152
|
+
await withAuth(options(), dependencies, async ({ auth }) => {
|
|
153
|
+
dependencies.writeJson(clientInfo(auth));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return program;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Parse `argv` and run the selected auth operation. */
|
|
161
|
+
export async function runCli(argv: string[]): Promise<void> {
|
|
162
|
+
await buildProgram().parseAsync(argv);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { CommanderError };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Databricks auth command-line option schemas and runtime projection.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AuthOptionsSchema,
|
|
9
|
+
DatabricksAuthOptionsSchema,
|
|
10
|
+
type DatabricksAuthOptions,
|
|
11
|
+
} from "@dbx-tools/auth/config";
|
|
12
|
+
import { stringUtils } from "@dbx-tools/shared-core";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
export const AuthCliOptionsSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
profile: DatabricksAuthOptionsSchema.shape.profile,
|
|
18
|
+
host: DatabricksAuthOptionsSchema.shape.host,
|
|
19
|
+
accountId: DatabricksAuthOptionsSchema.shape.accountId,
|
|
20
|
+
workspaceId: DatabricksAuthOptionsSchema.shape.workspaceId,
|
|
21
|
+
configFile: DatabricksAuthOptionsSchema.shape.configFile,
|
|
22
|
+
clientId: DatabricksAuthOptionsSchema.shape.clientId,
|
|
23
|
+
groupId: DatabricksAuthOptionsSchema.shape.groupId,
|
|
24
|
+
authType: DatabricksAuthOptionsSchema.shape.authType,
|
|
25
|
+
scopes: z
|
|
26
|
+
.preprocess(
|
|
27
|
+
(value) =>
|
|
28
|
+
Array.isArray(value)
|
|
29
|
+
? value.flatMap((entry) => stringUtils.parseList(String(entry)))
|
|
30
|
+
: value,
|
|
31
|
+
z.array(z.string().trim().min(1)),
|
|
32
|
+
)
|
|
33
|
+
.default([])
|
|
34
|
+
.describe("OAuth scopes."),
|
|
35
|
+
target: DatabricksAuthOptionsSchema.shape.target,
|
|
36
|
+
lockTimeoutMs: AuthOptionsSchema.shape.lockTimeoutMs,
|
|
37
|
+
loginTimeoutMs: AuthOptionsSchema.shape.loginTimeoutMs,
|
|
38
|
+
refreshBufferMs: AuthOptionsSchema.shape.refreshBufferMs,
|
|
39
|
+
preferUserToMachine: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.default(true)
|
|
42
|
+
.describe("Prefer a matching user profile over selected machine credentials."),
|
|
43
|
+
})
|
|
44
|
+
.strict()
|
|
45
|
+
.describe("Options shared by every Databricks auth command.");
|
|
46
|
+
|
|
47
|
+
export const TokenCommandOptionsSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
forceRefresh: z.boolean().default(false).describe("Refresh the token before returning it."),
|
|
50
|
+
login: z.boolean().default(true).describe("Log in when credentials are missing or invalid."),
|
|
51
|
+
})
|
|
52
|
+
.strict()
|
|
53
|
+
.describe("Options for returning a Databricks access token.");
|
|
54
|
+
|
|
55
|
+
export type AuthCliOptions = z.output<typeof AuthCliOptionsSchema>;
|
|
56
|
+
|
|
57
|
+
export type TokenCommandOptions = z.output<typeof TokenCommandOptionsSchema>;
|
|
58
|
+
|
|
59
|
+
/** Convert flat CLI fields into the Node auth package's nested options. */
|
|
60
|
+
export function databricksAuthOptions(options: AuthCliOptions): DatabricksAuthOptions {
|
|
61
|
+
return {
|
|
62
|
+
profile: options.profile,
|
|
63
|
+
host: options.host,
|
|
64
|
+
accountId: options.accountId,
|
|
65
|
+
workspaceId: options.workspaceId,
|
|
66
|
+
configFile: options.configFile,
|
|
67
|
+
clientId: options.clientId,
|
|
68
|
+
groupId: options.groupId,
|
|
69
|
+
authType: options.authType,
|
|
70
|
+
scopes: options.scopes.length > 0 ? options.scopes : undefined,
|
|
71
|
+
target: options.target,
|
|
72
|
+
auth: {
|
|
73
|
+
lockTimeoutMs: options.lockTimeoutMs,
|
|
74
|
+
loginTimeoutMs: options.loginTimeoutMs,
|
|
75
|
+
refreshBufferMs: options.refreshBufferMs,
|
|
76
|
+
},
|
|
77
|
+
preferUserToMachine: options.preferUserToMachine,
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -106,47 +106,63 @@ export function buildProgram(name: string = PROGRAM_NAMES[0]): Command {
|
|
|
106
106
|
program,
|
|
107
107
|
"appkit",
|
|
108
108
|
"AppKit helpers (env: print the environment an AppKit app resolves)",
|
|
109
|
-
async () => (await import("
|
|
109
|
+
async () => (await import("./appkit/cli.ts")).buildProgram,
|
|
110
110
|
);
|
|
111
111
|
|
|
112
112
|
addForwardedCommand(
|
|
113
113
|
program,
|
|
114
114
|
"auth",
|
|
115
115
|
"Authenticate to Databricks and manage OAuth tokens",
|
|
116
|
-
async () => (await import("
|
|
116
|
+
async () => (await import("./auth/cli.ts")).buildProgram,
|
|
117
117
|
);
|
|
118
118
|
|
|
119
119
|
addForwardedCommand(
|
|
120
120
|
program,
|
|
121
121
|
"tunnel",
|
|
122
122
|
"Run a public portr tunnel with an email-OTP gate",
|
|
123
|
-
async () => (await import("
|
|
123
|
+
async () => (await import("./tunnel/cli.ts")).buildProgram,
|
|
124
124
|
);
|
|
125
125
|
|
|
126
126
|
addForwardedCommand(
|
|
127
127
|
program,
|
|
128
128
|
"lakebase-proxy",
|
|
129
129
|
"Run the Node Databricks Lakebase PostgreSQL proxy",
|
|
130
|
-
async () => (await import("
|
|
130
|
+
async () => (await import("./lakebase-proxy/cli.ts")).buildProgram,
|
|
131
131
|
);
|
|
132
132
|
|
|
133
133
|
addForwardedCommand(
|
|
134
134
|
program,
|
|
135
135
|
"model-gateway",
|
|
136
136
|
"Run the foreground AppKit Databricks model gateway",
|
|
137
|
-
async () => (await import("
|
|
137
|
+
async () => (await import("./model-gateway/cli.ts")).buildProgram,
|
|
138
138
|
);
|
|
139
139
|
|
|
140
140
|
addForwardedCommand(
|
|
141
141
|
program,
|
|
142
142
|
"graphiti",
|
|
143
143
|
"Run Graphiti or manage its current-user desktop service",
|
|
144
|
-
async () => (await import("
|
|
144
|
+
async () => (await import("./graphiti/cli.ts")).buildProgram,
|
|
145
145
|
);
|
|
146
146
|
|
|
147
147
|
return program;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/** Build the complete command tree used for generated documentation. */
|
|
151
|
+
export async function buildDocumentationProgram(name: string = PROGRAM_NAMES[0]): Promise<Command> {
|
|
152
|
+
const program = buildProgram(name);
|
|
153
|
+
(program.commands as Command[]).splice(
|
|
154
|
+
1,
|
|
155
|
+
program.commands.length - 1,
|
|
156
|
+
(await import("./appkit/cli.ts")).buildProgram("appkit"),
|
|
157
|
+
(await import("./auth/cli.ts")).buildProgram("auth"),
|
|
158
|
+
(await import("./tunnel/cli.ts")).buildProgram("tunnel"),
|
|
159
|
+
(await import("./lakebase-proxy/cli.ts")).buildProgram("lakebase-proxy"),
|
|
160
|
+
(await import("./model-gateway/cli.ts")).buildProgram("model-gateway"),
|
|
161
|
+
(await import("./graphiti/cli.ts")).buildProgram("graphiti"),
|
|
162
|
+
);
|
|
163
|
+
return program;
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
/**
|
|
151
167
|
* Program name for help output: whichever bin the user actually typed, so
|
|
152
168
|
* `dbx-tools --help` doesn't document itself as `dbx`. Falls back to `dbx`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander-owned Graphiti CLI and shared current-user service definition.
|
|
3
|
+
*
|
|
4
|
+
* This module owns every user-facing Graphiti command. The shared Graphiti Zod
|
|
5
|
+
* schema owns its options, environment names, defaults, and validation. The
|
|
6
|
+
* Node Graphiti package owns runtime and Python interaction.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { addArgs, parseArgs, serializeArgs } from "@dbx-tools/cli-args";
|
|
11
|
+
import { buildServiceCommand, type CliServiceCliDependencies } from "@dbx-tools/cli-service/cli";
|
|
12
|
+
import { defineService, type CliServiceDefinition } from "@dbx-tools/cli-service/definition";
|
|
13
|
+
import { runGraphiti, type GraphitiRuntimeOptions } from "@dbx-tools/graphiti/runtime";
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
|
|
16
|
+
import { PACKAGE_VERSION } from "../../index.ts";
|
|
17
|
+
import {
|
|
18
|
+
GraphitiCliOptionsSchema,
|
|
19
|
+
GraphitiOptionsSchema,
|
|
20
|
+
type GraphitiOptions,
|
|
21
|
+
} from "./options.ts";
|
|
22
|
+
|
|
23
|
+
/** Injectable runtime and service lifecycle boundaries for CLI callers. */
|
|
24
|
+
export interface GraphitiCliDependencies {
|
|
25
|
+
/** Run the Graphiti stack through the Node runtime owner. */
|
|
26
|
+
run(options: GraphitiRuntimeOptions): Promise<void>;
|
|
27
|
+
/** Shared service command dependencies for tests and embedding. */
|
|
28
|
+
readonly service?: CliServiceCliDependencies;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Runtime options persisted when installing the Graphiti desktop service. */
|
|
32
|
+
export type GraphitiServiceOptions = GraphitiOptions;
|
|
33
|
+
|
|
34
|
+
/** Define a Graphiti service without duplicating installation or lifecycle policy. */
|
|
35
|
+
export function graphitiServiceDefinition(
|
|
36
|
+
options: GraphitiServiceOptions = {},
|
|
37
|
+
): CliServiceDefinition {
|
|
38
|
+
return defineService(import.meta.url, {
|
|
39
|
+
id: "dbx-tools.cli-graphiti",
|
|
40
|
+
name: "dbx graphiti",
|
|
41
|
+
pythonPackage: { name: "dbx-tools-graphiti" },
|
|
42
|
+
command: {
|
|
43
|
+
binName: "dbx-graphiti",
|
|
44
|
+
arguments: serializeArgs(GraphitiOptionsSchema.parse(options)),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Build foreground execution and shared desktop-service commands. */
|
|
50
|
+
export function buildProgram(
|
|
51
|
+
name = "dbx graphiti",
|
|
52
|
+
dependencies: GraphitiCliDependencies = { run: runGraphiti },
|
|
53
|
+
): Command {
|
|
54
|
+
const program = addArgs(
|
|
55
|
+
new Command()
|
|
56
|
+
.name(name)
|
|
57
|
+
.description("Run Graphiti or manage its current-user desktop service")
|
|
58
|
+
.version(PACKAGE_VERSION, "-v, --version"),
|
|
59
|
+
GraphitiCliOptionsSchema,
|
|
60
|
+
).action(async () => {
|
|
61
|
+
await dependencies.run(graphitiOptions(program));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
program.addCommand(
|
|
65
|
+
buildServiceCommand(
|
|
66
|
+
() => graphitiServiceDefinition(graphitiOptions(program)),
|
|
67
|
+
dependencies.service,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
return program;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function graphitiOptions(command: Command): GraphitiOptions {
|
|
74
|
+
return GraphitiOptionsSchema.parse(parseArgs(command, GraphitiCliOptionsSchema));
|
|
75
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphiti CLI option surface.
|
|
3
|
+
*
|
|
4
|
+
* The Node Graphiti runtime owns the complete shared option contract. This
|
|
5
|
+
* module re-exports that surface without creating parallel CLI policy.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
GRAPHITI_DEFAULTS,
|
|
12
|
+
GraphitiCliOptionsSchema,
|
|
13
|
+
GraphitiOptionsSchema,
|
|
14
|
+
ResolvedGraphitiOptionsSchema,
|
|
15
|
+
graphitiOptionOverrides,
|
|
16
|
+
graphitiOptionsEnvironment,
|
|
17
|
+
graphitiOptionsFromEnvironment,
|
|
18
|
+
resolveGraphitiOptions,
|
|
19
|
+
} from "@dbx-tools/graphiti/options";
|
|
20
|
+
export type { GraphitiOptions, ResolvedGraphitiOptions } from "@dbx-tools/graphiti/options";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** PostgreSQL cancellation key translation and TLS forwarding. */
|
|
2
|
+
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { createConnection, isIP, type Socket } from "node:net";
|
|
5
|
+
import { connect as connectTls, type TLSSocket } from "node:tls";
|
|
6
|
+
|
|
7
|
+
import { cancelRequest, sslRequest, type CancelMessage } from "./protocol.ts";
|
|
8
|
+
|
|
9
|
+
/** Upstream PostgreSQL backend key used to forward a cancellation request. */
|
|
10
|
+
export interface CancellationTarget {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
processId: number;
|
|
14
|
+
secretKey: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Sends one PostgreSQL cancellation request to its upstream backend. */
|
|
18
|
+
export type CancellationForwarder = (target: CancellationTarget) => Promise<void>;
|
|
19
|
+
|
|
20
|
+
let nextProcessId = 10_000;
|
|
21
|
+
|
|
22
|
+
/** Maps synthetic local backend keys onto real upstream cancellation targets. */
|
|
23
|
+
export class CancellationRegistry {
|
|
24
|
+
private readonly targets = new Map<string, CancellationTarget>();
|
|
25
|
+
|
|
26
|
+
constructor(private readonly forwarder: CancellationForwarder = forwardCancellation) {}
|
|
27
|
+
|
|
28
|
+
register(target: CancellationTarget): CancelMessage {
|
|
29
|
+
const processId = nextProcessId++;
|
|
30
|
+
const secretKey = randomBytes(4).readInt32BE(0);
|
|
31
|
+
this.targets.set(key(processId, secretKey), target);
|
|
32
|
+
return { kind: "cancel", processId, secretKey };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
remove(message: CancelMessage): void {
|
|
36
|
+
this.targets.delete(key(message.processId, message.secretKey));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async forward(message: CancelMessage): Promise<boolean> {
|
|
40
|
+
const target = this.targets.get(key(message.processId, message.secretKey));
|
|
41
|
+
if (!target) return false;
|
|
42
|
+
await this.forwarder(target);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Forward a PostgreSQL cancellation request over a verified TLS connection. */
|
|
48
|
+
export async function forwardCancellation(target: CancellationTarget): Promise<void> {
|
|
49
|
+
const socket = createConnection({ host: target.host, port: target.port });
|
|
50
|
+
await event(socket, "connect");
|
|
51
|
+
socket.write(sslRequest());
|
|
52
|
+
const response = await readByte(socket);
|
|
53
|
+
if (response !== "S".charCodeAt(0)) {
|
|
54
|
+
socket.destroy();
|
|
55
|
+
throw new Error("Lakebase upstream refused TLS for cancellation");
|
|
56
|
+
}
|
|
57
|
+
const tls = connectTls({
|
|
58
|
+
socket,
|
|
59
|
+
rejectUnauthorized: true,
|
|
60
|
+
...(isIP(target.host) === 0 ? { servername: target.host } : {}),
|
|
61
|
+
});
|
|
62
|
+
await event(tls, "secureConnect");
|
|
63
|
+
await writeAndClose(tls, cancelRequest(target.processId, target.secretKey));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function key(processId: number, secretKey: number): string {
|
|
67
|
+
return `${processId}:${secretKey}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function event(socket: Socket | TLSSocket, name: "connect" | "secureConnect"): Promise<void> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const onEvent = () => finish();
|
|
73
|
+
const onError = (error: Error) => finish(error);
|
|
74
|
+
const finish = (error?: Error) => {
|
|
75
|
+
socket.off(name, onEvent);
|
|
76
|
+
socket.off("error", onError);
|
|
77
|
+
if (error) reject(error);
|
|
78
|
+
else resolve();
|
|
79
|
+
};
|
|
80
|
+
socket.once(name, onEvent);
|
|
81
|
+
socket.once("error", onError);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readByte(socket: Socket): Promise<number> {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const onData = (chunk: Buffer) => {
|
|
88
|
+
finish();
|
|
89
|
+
if (chunk.length > 1) socket.unshift(chunk.subarray(1));
|
|
90
|
+
resolve(chunk[0]!);
|
|
91
|
+
};
|
|
92
|
+
const onError = (error: Error) => {
|
|
93
|
+
finish();
|
|
94
|
+
reject(error);
|
|
95
|
+
};
|
|
96
|
+
const onEnd = () => {
|
|
97
|
+
finish();
|
|
98
|
+
reject(new Error("Lakebase upstream closed during TLS negotiation"));
|
|
99
|
+
};
|
|
100
|
+
const finish = () => {
|
|
101
|
+
socket.off("data", onData);
|
|
102
|
+
socket.off("error", onError);
|
|
103
|
+
socket.off("end", onEnd);
|
|
104
|
+
};
|
|
105
|
+
socket.once("data", onData);
|
|
106
|
+
socket.once("error", onError);
|
|
107
|
+
socket.once("end", onEnd);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeAndClose(socket: TLSSocket, packet: Buffer): Promise<void> {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
socket.end(packet, () => resolve());
|
|
114
|
+
socket.once("error", reject);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Commander entry point for the pure Node Lakebase proxy. */
|
|
2
|
+
|
|
3
|
+
import { addArgs, parseArgs, serializeArgs } from "@dbx-tools/cli-args";
|
|
4
|
+
import { buildServiceCommand, type CliServiceCliDependencies } from "@dbx-tools/cli-service/cli";
|
|
5
|
+
import { defineService, type CliServiceDefinition } from "@dbx-tools/cli-service/definition";
|
|
6
|
+
import { connectionUrl } from "@dbx-tools/lakebase";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
import { PACKAGE_VERSION } from "../../index.ts";
|
|
10
|
+
import {
|
|
11
|
+
LakebaseProxyOptionsSchema,
|
|
12
|
+
LakebaseProxyUrlOptionsSchema,
|
|
13
|
+
resolveLakebaseProxyOptions,
|
|
14
|
+
type LakebaseProxyOptions,
|
|
15
|
+
} from "./options.ts";
|
|
16
|
+
import { LakebaseProxy } from "./proxy.ts";
|
|
17
|
+
|
|
18
|
+
/** Injectable service boundary for Lakebase proxy CLI composition and tests. */
|
|
19
|
+
export interface LakebaseProxyCliDependencies {
|
|
20
|
+
readonly service?: CliServiceCliDependencies;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build the tray-only Lakebase proxy service definition. */
|
|
24
|
+
export function lakebaseProxyServiceDefinition(
|
|
25
|
+
options: LakebaseProxyOptions = {},
|
|
26
|
+
): CliServiceDefinition {
|
|
27
|
+
const resolved = resolveLakebaseProxyOptions(options);
|
|
28
|
+
return defineService(import.meta.url, {
|
|
29
|
+
id: "dbx-tools.cli-lakebase-proxy",
|
|
30
|
+
name: "dbx lakebase proxy",
|
|
31
|
+
command: {
|
|
32
|
+
binName: "dbx-lakebase-proxy",
|
|
33
|
+
arguments: serializeArgs(resolved),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build the Lakebase proxy command-line program. */
|
|
39
|
+
export function buildProgram(
|
|
40
|
+
name = "dbx lakebase-proxy",
|
|
41
|
+
dependencies: LakebaseProxyCliDependencies = {},
|
|
42
|
+
): Command {
|
|
43
|
+
const program = addArgs(
|
|
44
|
+
new Command(name)
|
|
45
|
+
.description("Run a loopback PostgreSQL proxy for Databricks Lakebase")
|
|
46
|
+
.version(PACKAGE_VERSION)
|
|
47
|
+
.enablePositionalOptions(),
|
|
48
|
+
LakebaseProxyOptionsSchema,
|
|
49
|
+
).action(async () => {
|
|
50
|
+
const proxy = new LakebaseProxy(parseArgs(program, LakebaseProxyOptionsSchema));
|
|
51
|
+
await proxy.listen();
|
|
52
|
+
await waitForShutdown();
|
|
53
|
+
await proxy.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const urlCommand = addArgs(
|
|
57
|
+
program.command("url").description("Format a local PostgreSQL URL for a Lakebase target"),
|
|
58
|
+
LakebaseProxyUrlOptionsSchema,
|
|
59
|
+
).action(() => {
|
|
60
|
+
const options = parseArgs(urlCommand, LakebaseProxyUrlOptionsSchema);
|
|
61
|
+
const target = options.target ?? options.lakebaseEndpoint!;
|
|
62
|
+
process.stdout.write(`${connectionUrl(target, options.listen.host, options.listen.port)}\n`);
|
|
63
|
+
});
|
|
64
|
+
let installCommand!: Command;
|
|
65
|
+
const serviceCommand = buildServiceCommand(() => {
|
|
66
|
+
return lakebaseProxyServiceDefinition(parseArgs(installCommand, LakebaseProxyOptionsSchema));
|
|
67
|
+
}, dependencies.service);
|
|
68
|
+
installCommand = serviceCommand.commands.find((command) => command.name() === "install")!;
|
|
69
|
+
addArgs(installCommand, LakebaseProxyOptionsSchema);
|
|
70
|
+
program.addCommand(serviceCommand);
|
|
71
|
+
return program;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function waitForShutdown(): Promise<void> {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const shutdown = () => resolve();
|
|
77
|
+
process.once("SIGINT", shutdown);
|
|
78
|
+
process.once("SIGTERM", shutdown);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lakebase proxy listener, service, and URL command options.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { options } from "@dbx-tools/shared-core";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
const text = (description: string) => z.string().trim().min(1).optional().describe(description);
|
|
11
|
+
|
|
12
|
+
export const LakebaseProxyOptionsSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
...options.PostgresOptionsSchema.shape,
|
|
15
|
+
listen: options
|
|
16
|
+
.listenAddressSchema({ port: 5432, loopback: true })
|
|
17
|
+
.describe("Loopback listener address."),
|
|
18
|
+
startupTimeoutSeconds: z.coerce
|
|
19
|
+
.number<number>()
|
|
20
|
+
.int()
|
|
21
|
+
.nonnegative()
|
|
22
|
+
.default(30)
|
|
23
|
+
.describe("Startup timeout in seconds."),
|
|
24
|
+
profile: options.DatabricksOptionsSchema.shape.profile.describe("Exact Databricks profile."),
|
|
25
|
+
})
|
|
26
|
+
.strict()
|
|
27
|
+
.describe("Lakebase proxy listener and service options.");
|
|
28
|
+
|
|
29
|
+
export const LakebaseProxyUrlOptionsSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
target: text("Lakebase project, resource path, host, or URL."),
|
|
32
|
+
lakebaseEndpoint: options.LakebaseOptionsSchema.shape.lakebaseEndpoint,
|
|
33
|
+
listen: options.listenAddressSchema({ port: 5432 }).describe("Local proxy address."),
|
|
34
|
+
})
|
|
35
|
+
.strict()
|
|
36
|
+
.refine(
|
|
37
|
+
(configured) => configured.target !== undefined || configured.lakebaseEndpoint !== undefined,
|
|
38
|
+
{
|
|
39
|
+
message: "URL requires target or endpoint.",
|
|
40
|
+
path: ["target"],
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
.describe("Options for formatting a local Lakebase PostgreSQL URL.");
|
|
44
|
+
|
|
45
|
+
export type LakebaseProxyOptions = z.input<typeof LakebaseProxyOptionsSchema>;
|
|
46
|
+
|
|
47
|
+
export type ResolvedLakebaseProxyOptions = z.output<typeof LakebaseProxyOptionsSchema>;
|
|
48
|
+
|
|
49
|
+
export type LakebaseProxyUrlOptions = z.output<typeof LakebaseProxyUrlOptionsSchema>;
|
|
50
|
+
|
|
51
|
+
/** Validate and default Lakebase proxy listener options. */
|
|
52
|
+
export function resolveLakebaseProxyOptions(
|
|
53
|
+
options: LakebaseProxyOptions = {},
|
|
54
|
+
): ResolvedLakebaseProxyOptions {
|
|
55
|
+
return LakebaseProxyOptionsSchema.parse(options);
|
|
56
|
+
}
|