driggsby 0.1.10 → 0.1.11

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 CHANGED
@@ -18,6 +18,10 @@ Node `20+` is required.
18
18
 
19
19
  If you prefer not to install globally, you can also use `npx -y driggsby ...`.
20
20
 
21
+ On machines without working platform keyring support, such as some headless
22
+ Linux servers, Driggsby falls back to an owner-only local file-backed secret
23
+ store so the CLI can still complete login and run the broker.
24
+
21
25
  ## Quick Start
22
26
 
23
27
  1. Sign in:
@@ -2,7 +2,7 @@ import { assertBrokerRemoteUrl } from "./url-security.js";
2
2
  const DEFAULT_REMOTE_BASE_URL = "https://app.driggsby.com";
3
3
  const DEFAULT_SCOPE = "driggsby.default";
4
4
  const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1_000;
5
- const DEFAULT_CLIENT_NAME = "Driggsby Local Broker";
5
+ const DEFAULT_CLIENT_NAME = "Driggsby CLI";
6
6
  export function resolveBrokerAuthConfig(env = process.env) {
7
7
  const remoteBaseUrl = normalizeBaseUrl(env["DRIGGSBY_REMOTE_BASE_URL"] ?? DEFAULT_REMOTE_BASE_URL);
8
8
  return {
@@ -1,11 +1,11 @@
1
1
  import { buildBrokerStatus, ensureBrokerInstallation, readBrokerDpopKeyPair, readBrokerLocalAuthToken, readBrokerPrivateJwk, } from "./installation.js";
2
2
  import { BrokerRemoteSessionManager } from "./remote-session.js";
3
3
  import { callRemoteTool, listRemoteTools } from "./remote-mcp.js";
4
- import { KeyringSecretStore } from "./secret-store.js";
4
+ import { resolveSecretStore } from "./resolve-secret-store.js";
5
5
  import { LocalBrokerServer } from "./server.js";
6
6
  import { buildReauthenticationRequiredMessage } from "../lib/user-guidance.js";
7
7
  export async function runBrokerDaemon(runtimePaths) {
8
- const secretStore = new KeyringSecretStore();
8
+ const secretStore = (await resolveSecretStore(runtimePaths)).store;
9
9
  const metadata = await ensureBrokerInstallation(runtimePaths, secretStore);
10
10
  const localAuthToken = await readRequiredLocalAuthToken(secretStore, metadata.brokerId);
11
11
  const privateJwk = await readRequiredPrivateJwk(secretStore, metadata.brokerId);
@@ -0,0 +1,130 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { readJsonFile, removeFileIfPresent, writeJsonFile, } from "../lib/json-file.js";
5
+ const FILE_SECRET_KEY_BYTES = 32;
6
+ const FILE_SECRET_STORE_INCOMPLETE_MESSAGE = "The local Driggsby file-backed secret store is incomplete. Run `driggsby logout` and then `driggsby login`.";
7
+ const FILE_SECRET_STORE_INVALID_MESSAGE = "The local Driggsby file-backed secret store is invalid. Run `driggsby logout` and then `driggsby login`.";
8
+ const FILE_SECRET_STORE_SCHEMA_VERSION = 1;
9
+ const FILE_SECRET_IV_BYTES = 12;
10
+ export class FileSecretStore {
11
+ encryptionKeyPath;
12
+ secretsPath;
13
+ constructor(runtimePaths) {
14
+ this.encryptionKeyPath = path.join(runtimePaths.stateDir, "broker-secrets.key");
15
+ this.secretsPath = path.join(runtimePaths.configDir, "broker-secrets.json");
16
+ }
17
+ async hasStoredSecrets() {
18
+ const storedSecrets = await this.readStoredSecretsFile();
19
+ return Object.keys(storedSecrets?.secrets ?? {}).length > 0;
20
+ }
21
+ async setSecret(account, secret) {
22
+ const encryptionKey = await this.readOrCreateEncryptionKey();
23
+ const storedSecrets = (await this.readStoredSecretsFile()) ?? {
24
+ schemaVersion: FILE_SECRET_STORE_SCHEMA_VERSION,
25
+ secrets: {},
26
+ };
27
+ storedSecrets.secrets[account] = encryptSecret(secret, encryptionKey);
28
+ await writeJsonFile(this.secretsPath, storedSecrets);
29
+ }
30
+ async getSecret(account) {
31
+ const storedSecrets = await this.readStoredSecretsFile();
32
+ const encryptedSecret = storedSecrets?.secrets[account];
33
+ if (encryptedSecret === undefined) {
34
+ return null;
35
+ }
36
+ const encryptionKey = await this.readEncryptionKey(true);
37
+ if (encryptionKey === null) {
38
+ throw new Error(FILE_SECRET_STORE_INCOMPLETE_MESSAGE);
39
+ }
40
+ return decryptSecret(encryptedSecret, encryptionKey);
41
+ }
42
+ async deleteSecret(account) {
43
+ const storedSecrets = await this.readStoredSecretsFile();
44
+ if (storedSecrets?.secrets[account] === undefined) {
45
+ return false;
46
+ }
47
+ const remainingSecrets = Object.fromEntries(Object.entries(storedSecrets.secrets).filter(([storedAccount]) => storedAccount !== account));
48
+ if (Object.keys(remainingSecrets).length === 0) {
49
+ await Promise.all([
50
+ removeFileIfPresent(this.secretsPath),
51
+ removeFileIfPresent(this.encryptionKeyPath),
52
+ ]);
53
+ return true;
54
+ }
55
+ await writeJsonFile(this.secretsPath, {
56
+ ...storedSecrets,
57
+ secrets: remainingSecrets,
58
+ });
59
+ return true;
60
+ }
61
+ async readStoredSecretsFile() {
62
+ return await readJsonFile(this.secretsPath);
63
+ }
64
+ async readEncryptionKey(errorIfMissing) {
65
+ try {
66
+ const encodedKey = await fs.readFile(this.encryptionKeyPath, "utf8");
67
+ const encryptionKey = Buffer.from(encodedKey.trim(), "base64");
68
+ if (encryptionKey.length !== FILE_SECRET_KEY_BYTES) {
69
+ throw new Error(FILE_SECRET_STORE_INVALID_MESSAGE);
70
+ }
71
+ return encryptionKey;
72
+ }
73
+ catch (error) {
74
+ if (!errorIfMissing &&
75
+ error instanceof Error &&
76
+ "code" in error &&
77
+ error.code === "ENOENT") {
78
+ return null;
79
+ }
80
+ if (errorIfMissing &&
81
+ error instanceof Error &&
82
+ "code" in error &&
83
+ error.code === "ENOENT") {
84
+ throw new Error(FILE_SECRET_STORE_INCOMPLETE_MESSAGE);
85
+ }
86
+ throw error;
87
+ }
88
+ }
89
+ async readOrCreateEncryptionKey() {
90
+ const existingKey = await this.readEncryptionKey(false);
91
+ if (existingKey !== null) {
92
+ return existingKey;
93
+ }
94
+ await fs.mkdir(path.dirname(this.secretsPath), {
95
+ recursive: true,
96
+ mode: 0o700,
97
+ });
98
+ await fs.mkdir(path.dirname(this.encryptionKeyPath), {
99
+ recursive: true,
100
+ mode: 0o700,
101
+ });
102
+ const encryptionKey = randomBytes(FILE_SECRET_KEY_BYTES);
103
+ await fs.writeFile(this.encryptionKeyPath, encryptionKey.toString("base64"), {
104
+ encoding: "utf8",
105
+ mode: 0o600,
106
+ });
107
+ return encryptionKey;
108
+ }
109
+ }
110
+ function encryptSecret(secret, encryptionKey) {
111
+ const iv = randomBytes(FILE_SECRET_IV_BYTES);
112
+ const cipher = createCipheriv("aes-256-gcm", encryptionKey, iv);
113
+ const ciphertext = Buffer.concat([
114
+ cipher.update(secret, "utf8"),
115
+ cipher.final(),
116
+ ]);
117
+ return {
118
+ authTagBase64: cipher.getAuthTag().toString("base64"),
119
+ ciphertextBase64: ciphertext.toString("base64"),
120
+ ivBase64: iv.toString("base64"),
121
+ };
122
+ }
123
+ function decryptSecret(encryptedSecret, encryptionKey) {
124
+ const decipher = createDecipheriv("aes-256-gcm", encryptionKey, Buffer.from(encryptedSecret.ivBase64, "base64"));
125
+ decipher.setAuthTag(Buffer.from(encryptedSecret.authTagBase64, "base64"));
126
+ return Buffer.concat([
127
+ decipher.update(Buffer.from(encryptedSecret.ciphertextBase64, "base64")),
128
+ decipher.final(),
129
+ ]).toString("utf8");
130
+ }
@@ -0,0 +1,34 @@
1
+ import { randomBytes } from "node:crypto";
2
+ export class KeyringSecretStore {
3
+ serviceName;
4
+ constructor(serviceName = "driggsby.local-broker") {
5
+ this.serviceName = serviceName;
6
+ }
7
+ async isAvailable() {
8
+ try {
9
+ const AsyncEntry = await loadAsyncEntry();
10
+ await new AsyncEntry(this.serviceName, `driggsby-probe-${randomBytes(12).toString("hex")}`).getPassword();
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ async setSecret(account, secret) {
18
+ const AsyncEntry = await loadAsyncEntry();
19
+ await new AsyncEntry(this.serviceName, account).setPassword(secret);
20
+ }
21
+ async getSecret(account) {
22
+ const AsyncEntry = await loadAsyncEntry();
23
+ const secret = await new AsyncEntry(this.serviceName, account).getPassword();
24
+ return secret ?? null;
25
+ }
26
+ async deleteSecret(account) {
27
+ const AsyncEntry = await loadAsyncEntry();
28
+ return await new AsyncEntry(this.serviceName, account).deleteCredential();
29
+ }
30
+ }
31
+ async function loadAsyncEntry() {
32
+ const keyringModule = await import("@napi-rs/keyring");
33
+ return keyringModule.AsyncEntry;
34
+ }
@@ -0,0 +1,52 @@
1
+ import { readBrokerMetadata } from "./installation.js";
2
+ import { FileSecretStore } from "./file-secret-store.js";
3
+ import { KeyringSecretStore } from "./keyring-secret-store.js";
4
+ const KEYRING_UNAVAILABLE_WITH_EXISTING_INSTALL_MESSAGE = "This Driggsby broker install already depends on platform secure storage, but that storage is unavailable in this shell. Reopen the original desktop session or restore keyring access before using this install.";
5
+ const LOGOUT_FALLBACK_NOTICE = "Platform secure storage is unavailable here, so logout will clear local broker files only. Any unreachable platform-keyring entries will remain until you return to the original session.";
6
+ export class ExistingInstallRequiresKeyringError extends Error {
7
+ constructor() {
8
+ super(KEYRING_UNAVAILABLE_WITH_EXISTING_INSTALL_MESSAGE);
9
+ this.name = "ExistingInstallRequiresKeyringError";
10
+ }
11
+ }
12
+ export async function resolveSecretStore(runtimePaths, options = {}) {
13
+ const fileStore = options.fileStore ?? new FileSecretStore(runtimePaths);
14
+ if (await fileStore.hasStoredSecrets()) {
15
+ return {
16
+ backend: "file",
17
+ notice: null,
18
+ store: fileStore,
19
+ };
20
+ }
21
+ const keyringStore = options.keyringStore ?? new KeyringSecretStore();
22
+ if (await keyringStore.isAvailable()) {
23
+ return {
24
+ backend: "keyring",
25
+ notice: null,
26
+ store: keyringStore,
27
+ };
28
+ }
29
+ if ((await readBrokerMetadata(runtimePaths)) !== null) {
30
+ throw new ExistingInstallRequiresKeyringError();
31
+ }
32
+ return {
33
+ backend: "file",
34
+ notice: `Platform secure storage is unavailable here. Driggsby will use an owner-only file-backed secret store under ${runtimePaths.configDir}.`,
35
+ store: fileStore,
36
+ };
37
+ }
38
+ export async function resolveSecretStoreForLogout(runtimePaths, options = {}) {
39
+ try {
40
+ return await resolveSecretStore(runtimePaths, options);
41
+ }
42
+ catch (error) {
43
+ if (!(error instanceof ExistingInstallRequiresKeyringError)) {
44
+ throw error;
45
+ }
46
+ return {
47
+ backend: "file",
48
+ notice: LOGOUT_FALLBACK_NOTICE,
49
+ store: options.fileStore ?? new FileSecretStore(runtimePaths),
50
+ };
51
+ }
52
+ }
@@ -1,20 +1,3 @@
1
- import { AsyncEntry } from "@napi-rs/keyring";
2
- export class KeyringSecretStore {
3
- serviceName;
4
- constructor(serviceName = "driggsby.local-broker") {
5
- this.serviceName = serviceName;
6
- }
7
- async setSecret(account, secret) {
8
- await new AsyncEntry(this.serviceName, account).setPassword(secret);
9
- }
10
- async getSecret(account) {
11
- const secret = await new AsyncEntry(this.serviceName, account).getPassword();
12
- return secret ?? null;
13
- }
14
- async deleteSecret(account) {
15
- return await new AsyncEntry(this.serviceName, account).deleteCredential();
16
- }
17
- }
18
1
  export class MemorySecretStore {
19
2
  secrets = new Map();
20
3
  setSecret(account, secret) {
@@ -1,10 +1,14 @@
1
1
  import { loginBroker } from "../../auth/login.js";
2
- import { KeyringSecretStore } from "../../broker/secret-store.js";
2
+ import { resolveSecretStore } from "../../broker/resolve-secret-store.js";
3
3
  import { ensureRuntimeDirectories } from "../../lib/runtime-paths.js";
4
4
  export async function runLoginCommand(runtimePaths) {
5
5
  await ensureRuntimeDirectories(runtimePaths);
6
- const secretStore = new KeyringSecretStore();
6
+ const resolvedSecretStore = await resolveSecretStore(runtimePaths);
7
+ const secretStore = resolvedSecretStore.store;
7
8
  process.stdout.write("Opening Driggsby sign-in in your browser...\n");
9
+ if (resolvedSecretStore.notice !== null) {
10
+ process.stdout.write(`${resolvedSecretStore.notice}\n`);
11
+ }
8
12
  const result = await loginBroker(runtimePaths, {
9
13
  onBrowserPrompt: ({ browserOpened, signInUrl }) => {
10
14
  if (!browserOpened) {
@@ -1,8 +1,9 @@
1
1
  import { getBrokerStatus, shutdownBroker } from "../../broker/client.js";
2
2
  import { clearBrokerInstallation } from "../../broker/installation.js";
3
- import { KeyringSecretStore } from "../../broker/secret-store.js";
3
+ import { resolveSecretStoreForLogout } from "../../broker/resolve-secret-store.js";
4
4
  export async function runLogoutCommand(runtimePaths) {
5
- const secretStore = new KeyringSecretStore();
5
+ const resolvedSecretStore = await resolveSecretStoreForLogout(runtimePaths);
6
+ const secretStore = resolvedSecretStore.store;
6
7
  const clientOptions = {
7
8
  runtimePaths,
8
9
  secretStore,
@@ -12,5 +13,8 @@ export async function runLogoutCommand(runtimePaths) {
12
13
  throw new Error("The local Driggsby broker did not shut down cleanly. Close active MCP sessions and try again.");
13
14
  }
14
15
  await clearBrokerInstallation(runtimePaths, secretStore);
16
+ if (resolvedSecretStore.notice !== null) {
17
+ process.stdout.write(`${resolvedSecretStore.notice}\n`);
18
+ }
15
19
  process.stdout.write("Local Driggsby broker state cleared.\n");
16
20
  }
@@ -1,9 +1,9 @@
1
1
  import { getBrokerStatus } from "../../broker/client.js";
2
2
  import { resolveBrokerStatusForDisplay } from "../../broker/installation.js";
3
- import { KeyringSecretStore } from "../../broker/secret-store.js";
3
+ import { resolveSecretStore } from "../../broker/resolve-secret-store.js";
4
4
  import { formatStatusText } from "../format.js";
5
5
  export async function runStatusCommand(runtimePaths) {
6
- const secretStore = new KeyringSecretStore();
6
+ const secretStore = (await resolveSecretStore(runtimePaths)).store;
7
7
  const liveStatus = await getBrokerStatus({
8
8
  runtimePaths,
9
9
  secretStore,
@@ -4,7 +4,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
4
4
  import { ensureBrokerRunning } from "../broker/launch.js";
5
5
  import { callBrokerTool, getBrokerStatus, listBrokerTools, } from "../broker/client.js";
6
6
  import { resolveBrokerStatusForDisplay } from "../broker/installation.js";
7
- import { KeyringSecretStore } from "../broker/secret-store.js";
7
+ import { resolveSecretStore } from "../broker/resolve-secret-store.js";
8
8
  import { retryOperation } from "../lib/retry.js";
9
9
  import { buildBrokerInvestigationMessage, errorMessageIncludesReauthenticationCommand, formatRetryWindow, } from "../lib/user-guidance.js";
10
10
  import { formatStatusText, } from "../cli/format.js";
@@ -20,7 +20,7 @@ const LOCAL_STATUS_TOOL = {
20
20
  };
21
21
  const BROKER_OPERATION_RETRY_DELAYS_MS = [0, 250, 500, 1_000, 2_000];
22
22
  export async function runMcpServerCommand(runtimePaths, entrypointPath) {
23
- const secretStore = new KeyringSecretStore();
23
+ const secretStore = (await resolveSecretStore(runtimePaths)).store;
24
24
  await ensureBrokerRunning({
25
25
  entrypointPath,
26
26
  runtimePaths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Local MCP broker and CLI for connecting AI clients to Driggsby",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",