sealnet-mcp 0.2.3 → 0.2.4

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
@@ -35,7 +35,7 @@ All state lives encrypted at `${XDG_CONFIG_HOME}/seal-mcp/state.json` (mode `0o6
35
35
 
36
36
  Passphrase delivery, in priority order:
37
37
 
38
- 1. **OS keychain** (default) — `keytar` (optional dependency) reads from libsecret / macOS Keychain / wincred. The first `serve` stores a random passphrase there itself; `sealnet-mcp init` lets you choose one instead.
38
+ 1. **OS keychain** (default) — `@napi-rs/keyring` (optional dependency) reads from the Secret Service on Linux / macOS Keychain / Windows Credential Manager. The first `serve` stores a random passphrase there itself; `sealnet-mcp init` lets you choose one instead.
39
39
  2. **`SEAL_MCP_PASSPHRASE` env var** — explicit opt-in for CI/headless. Warning logged.
40
40
  3. **Cleartext on disk** — explicit refusal even if user tries `--passphrase-file …`.
41
41
 
package/dist/cli.cjs CHANGED
@@ -7099,6 +7099,7 @@ async function probeStateLock(stateDir, deps = {}) {
7099
7099
  }
7100
7100
 
7101
7101
  // src/passphrase.ts
7102
+ var import_node_child_process = require("child_process");
7102
7103
  var import_node_crypto2 = require("crypto");
7103
7104
  var SERVICE_NAME = "seal-mcp";
7104
7105
  var ACCOUNT_NAME = "state";
@@ -7122,23 +7123,50 @@ var PassphraseNotFoundError = class extends Error {
7122
7123
  this.name = "PassphraseNotFoundError";
7123
7124
  }
7124
7125
  };
7126
+ function keyringEntry(keyring, platform, service, account) {
7127
+ return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
7128
+ }
7129
+ function readKeytarLinuxEntry(service, account) {
7130
+ return new Promise((resolve5) => {
7131
+ (0, import_node_child_process.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
7132
+ const value = err2 ? "" : stdout.replace(/\n$/, "");
7133
+ resolve5(value.length > 0 ? value : null);
7134
+ });
7135
+ });
7136
+ }
7137
+ function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
7138
+ const entry = (service, account) => keyringEntry(keyring, platform, service, account);
7139
+ const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
7140
+ return {
7141
+ async getPassword(service, account) {
7142
+ const secret = await entry(service, account).getSecret();
7143
+ if (secret !== void 0) return Buffer.from(secret).toString("utf8");
7144
+ const legacy = platform === "linux" ? await readLegacy(service, account) : null;
7145
+ if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
7146
+ return legacy;
7147
+ },
7148
+ setPassword: store,
7149
+ deletePassword: (service, account) => entry(service, account).deleteCredential()
7150
+ };
7151
+ }
7125
7152
  var keytarCache = void 0;
7126
7153
  async function loadKeytar() {
7127
7154
  if (keytarCache !== void 0) return keytarCache;
7128
7155
  try {
7129
- const mod = await import("keytar");
7130
- keytarCache = mod.default ?? mod;
7131
- return keytarCache;
7156
+ const mod = await import("@napi-rs/keyring");
7157
+ const keyring = mod.default ?? mod;
7158
+ keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
7159
+ keytarCache = keytarOverKeyring(keyring);
7132
7160
  } catch {
7133
7161
  keytarCache = null;
7134
- return null;
7135
7162
  }
7163
+ return keytarCache;
7136
7164
  }
7137
7165
  async function loadKeytarOrThrow() {
7138
7166
  const k = await loadKeytar();
7139
7167
  if (k === null) {
7140
7168
  throw new KeytarUnavailableError(
7141
- "failed to load `keytar` native module. On Debian/Ubuntu install `libsecret-1-0`; on Fedora install `gnome-keyring`; on macOS/Windows this should never happen \u2014 file a bug."
7169
+ "no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
7142
7170
  );
7143
7171
  }
7144
7172
  return k;
@@ -11797,7 +11825,7 @@ var seal_mcp_v1_default = {
11797
11825
  {
11798
11826
  rank: 1,
11799
11827
  method: "os_keychain",
11800
- implementation: "keytar (libsecret on Linux, macOS Keychain, Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
11828
+ implementation: "@napi-rs/keyring (the Secret Service on Linux, never the kernel keyring, which forgets on reboot; macOS Keychain; Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
11801
11829
  default: true
11802
11830
  },
11803
11831
  {
@@ -12945,7 +12973,7 @@ async function sealProFileGet(source, input) {
12945
12973
  }
12946
12974
 
12947
12975
  // src/tools/request.ts
12948
- var import_node_child_process = require("child_process");
12976
+ var import_node_child_process2 = require("child_process");
12949
12977
  var import_node_fs3 = require("fs");
12950
12978
  var import_promises5 = require("fs/promises");
12951
12979
  var import_node_os5 = require("os");
@@ -12964,7 +12992,7 @@ function runSealCli(args) {
12964
12992
  const cli = findSealCli();
12965
12993
  if (!cli) return Promise.resolve(null);
12966
12994
  return new Promise((resolve5) => {
12967
- (0, import_node_child_process.execFile)(cli, [...args], { maxBuffer: 1024 * 1024 }, (_err, stdout) => {
12995
+ (0, import_node_child_process2.execFile)(cli, [...args], { maxBuffer: 1024 * 1024 }, (_err, stdout) => {
12968
12996
  try {
12969
12997
  resolve5(JSON.parse(stdout));
12970
12998
  } catch {
@@ -13937,7 +13965,7 @@ function proFileGetOutputToWire(out) {
13937
13965
  }
13938
13966
 
13939
13967
  // src/server.ts
13940
- var SERVER_VERSION = "0.2.3";
13968
+ var SERVER_VERSION = "0.2.4";
13941
13969
  var toolMeta = (name) => {
13942
13970
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
13943
13971
  return { title, description, annotations };
@@ -14313,7 +14341,7 @@ async function runInit(opts) {
14313
14341
  if (err2 instanceof KeytarUnavailableError) {
14314
14342
  logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
14315
14343
  logStderr(
14316
- `${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until keytar works.`
14344
+ `${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
14317
14345
  );
14318
14346
  } else {
14319
14347
  throw err2;
@@ -14487,7 +14515,7 @@ async function runDoctor(opts) {
14487
14515
  }
14488
14516
  if (!kc.available) {
14489
14517
  logStderr(
14490
- `hint: keychain unavailable. Install \`libsecret-1-0\` (Debian/Ubuntu) or \`gnome-keyring\` (Fedora), or rely on ${PASSPHRASE_ENV_VAR}; with neither, \`serve\` runs ephemeral (handles live until restart).`
14518
+ `hint: keychain unavailable. On Linux start a Secret Service (gnome-keyring or KWallet), or rely on ${PASSPHRASE_ENV_VAR}; with neither, \`serve\` runs ephemeral (handles live until restart).`
14491
14519
  );
14492
14520
  } else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
14493
14521
  logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
package/dist/cli.js CHANGED
@@ -7071,6 +7071,7 @@ async function probeStateLock(stateDir, deps = {}) {
7071
7071
  }
7072
7072
 
7073
7073
  // src/passphrase.ts
7074
+ import { execFile } from "child_process";
7074
7075
  import { randomBytes as randomBytes2 } from "crypto";
7075
7076
  var SERVICE_NAME = "seal-mcp";
7076
7077
  var ACCOUNT_NAME = "state";
@@ -7094,23 +7095,50 @@ var PassphraseNotFoundError = class extends Error {
7094
7095
  this.name = "PassphraseNotFoundError";
7095
7096
  }
7096
7097
  };
7098
+ function keyringEntry(keyring, platform, service, account) {
7099
+ return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
7100
+ }
7101
+ function readKeytarLinuxEntry(service, account) {
7102
+ return new Promise((resolve5) => {
7103
+ execFile("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
7104
+ const value = err2 ? "" : stdout.replace(/\n$/, "");
7105
+ resolve5(value.length > 0 ? value : null);
7106
+ });
7107
+ });
7108
+ }
7109
+ function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
7110
+ const entry = (service, account) => keyringEntry(keyring, platform, service, account);
7111
+ const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
7112
+ return {
7113
+ async getPassword(service, account) {
7114
+ const secret = await entry(service, account).getSecret();
7115
+ if (secret !== void 0) return Buffer.from(secret).toString("utf8");
7116
+ const legacy = platform === "linux" ? await readLegacy(service, account) : null;
7117
+ if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
7118
+ return legacy;
7119
+ },
7120
+ setPassword: store,
7121
+ deletePassword: (service, account) => entry(service, account).deleteCredential()
7122
+ };
7123
+ }
7097
7124
  var keytarCache = void 0;
7098
7125
  async function loadKeytar() {
7099
7126
  if (keytarCache !== void 0) return keytarCache;
7100
7127
  try {
7101
- const mod = await import("keytar");
7102
- keytarCache = mod.default ?? mod;
7103
- return keytarCache;
7128
+ const mod = await import("@napi-rs/keyring");
7129
+ const keyring = mod.default ?? mod;
7130
+ keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
7131
+ keytarCache = keytarOverKeyring(keyring);
7104
7132
  } catch {
7105
7133
  keytarCache = null;
7106
- return null;
7107
7134
  }
7135
+ return keytarCache;
7108
7136
  }
7109
7137
  async function loadKeytarOrThrow() {
7110
7138
  const k = await loadKeytar();
7111
7139
  if (k === null) {
7112
7140
  throw new KeytarUnavailableError(
7113
- "failed to load `keytar` native module. On Debian/Ubuntu install `libsecret-1-0`; on Fedora install `gnome-keyring`; on macOS/Windows this should never happen \u2014 file a bug."
7141
+ "no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
7114
7142
  );
7115
7143
  }
7116
7144
  return k;
@@ -11769,7 +11797,7 @@ var seal_mcp_v1_default = {
11769
11797
  {
11770
11798
  rank: 1,
11771
11799
  method: "os_keychain",
11772
- implementation: "keytar (libsecret on Linux, macOS Keychain, Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
11800
+ implementation: "@napi-rs/keyring (the Secret Service on Linux, never the kernel keyring, which forgets on reboot; macOS Keychain; Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
11773
11801
  default: true
11774
11802
  },
11775
11803
  {
@@ -12917,7 +12945,7 @@ async function sealProFileGet(source, input) {
12917
12945
  }
12918
12946
 
12919
12947
  // src/tools/request.ts
12920
- import { execFile } from "child_process";
12948
+ import { execFile as execFile2 } from "child_process";
12921
12949
  import { existsSync } from "fs";
12922
12950
  import { mkdtemp as mkdtemp2 } from "fs/promises";
12923
12951
  import { tmpdir as tmpdir2 } from "os";
@@ -12936,7 +12964,7 @@ function runSealCli(args) {
12936
12964
  const cli = findSealCli();
12937
12965
  if (!cli) return Promise.resolve(null);
12938
12966
  return new Promise((resolve5) => {
12939
- execFile(cli, [...args], { maxBuffer: 1024 * 1024 }, (_err, stdout) => {
12967
+ execFile2(cli, [...args], { maxBuffer: 1024 * 1024 }, (_err, stdout) => {
12940
12968
  try {
12941
12969
  resolve5(JSON.parse(stdout));
12942
12970
  } catch {
@@ -13909,7 +13937,7 @@ function proFileGetOutputToWire(out) {
13909
13937
  }
13910
13938
 
13911
13939
  // src/server.ts
13912
- var SERVER_VERSION = "0.2.3";
13940
+ var SERVER_VERSION = "0.2.4";
13913
13941
  var toolMeta = (name) => {
13914
13942
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
13915
13943
  return { title, description, annotations };
@@ -14285,7 +14313,7 @@ async function runInit(opts) {
14285
14313
  if (err2 instanceof KeytarUnavailableError) {
14286
14314
  logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
14287
14315
  logStderr(
14288
- `${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until keytar works.`
14316
+ `${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
14289
14317
  );
14290
14318
  } else {
14291
14319
  throw err2;
@@ -14459,7 +14487,7 @@ async function runDoctor(opts) {
14459
14487
  }
14460
14488
  if (!kc.available) {
14461
14489
  logStderr(
14462
- `hint: keychain unavailable. Install \`libsecret-1-0\` (Debian/Ubuntu) or \`gnome-keyring\` (Fedora), or rely on ${PASSPHRASE_ENV_VAR}; with neither, \`serve\` runs ephemeral (handles live until restart).`
14490
+ `hint: keychain unavailable. On Linux start a Secret Service (gnome-keyring or KWallet), or rely on ${PASSPHRASE_ENV_VAR}; with neither, \`serve\` runs ephemeral (handles live until restart).`
14463
14491
  );
14464
14492
  } else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
14465
14493
  logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
package/dist/index.cjs CHANGED
@@ -4592,7 +4592,7 @@ var seal_mcp_v1_default = {
4592
4592
  {
4593
4593
  rank: 1,
4594
4594
  method: "os_keychain",
4595
- implementation: "keytar (libsecret on Linux, macOS Keychain, Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
4595
+ implementation: "@napi-rs/keyring (the Secret Service on Linux, never the kernel keyring, which forgets on reboot; macOS Keychain; Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
4596
4596
  default: true
4597
4597
  },
4598
4598
  {
@@ -13792,7 +13792,7 @@ function proFileGetOutputToWire(out) {
13792
13792
  }
13793
13793
 
13794
13794
  // src/server.ts
13795
- var SERVER_VERSION = "0.2.3";
13795
+ var SERVER_VERSION = "0.2.4";
13796
13796
  var toolMeta = (name) => {
13797
13797
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
13798
13798
  return { title, description, annotations };
@@ -14141,6 +14141,7 @@ function osDefaultDir(env, platform, home) {
14141
14141
  }
14142
14142
 
14143
14143
  // src/passphrase.ts
14144
+ var import_node_child_process2 = require("child_process");
14144
14145
  var import_node_crypto4 = require("crypto");
14145
14146
  var SERVICE_NAME = "seal-mcp";
14146
14147
  var ACCOUNT_NAME = "state";
@@ -14164,23 +14165,50 @@ var PassphraseNotFoundError = class extends Error {
14164
14165
  this.name = "PassphraseNotFoundError";
14165
14166
  }
14166
14167
  };
14168
+ function keyringEntry(keyring, platform, service, account) {
14169
+ return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
14170
+ }
14171
+ function readKeytarLinuxEntry(service, account) {
14172
+ return new Promise((resolve5) => {
14173
+ (0, import_node_child_process2.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
14174
+ const value = err2 ? "" : stdout.replace(/\n$/, "");
14175
+ resolve5(value.length > 0 ? value : null);
14176
+ });
14177
+ });
14178
+ }
14179
+ function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
14180
+ const entry = (service, account) => keyringEntry(keyring, platform, service, account);
14181
+ const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
14182
+ return {
14183
+ async getPassword(service, account) {
14184
+ const secret = await entry(service, account).getSecret();
14185
+ if (secret !== void 0) return Buffer.from(secret).toString("utf8");
14186
+ const legacy = platform === "linux" ? await readLegacy(service, account) : null;
14187
+ if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
14188
+ return legacy;
14189
+ },
14190
+ setPassword: store,
14191
+ deletePassword: (service, account) => entry(service, account).deleteCredential()
14192
+ };
14193
+ }
14167
14194
  var keytarCache = void 0;
14168
14195
  async function loadKeytar() {
14169
14196
  if (keytarCache !== void 0) return keytarCache;
14170
14197
  try {
14171
- const mod = await import("keytar");
14172
- keytarCache = mod.default ?? mod;
14173
- return keytarCache;
14198
+ const mod = await import("@napi-rs/keyring");
14199
+ const keyring = mod.default ?? mod;
14200
+ keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
14201
+ keytarCache = keytarOverKeyring(keyring);
14174
14202
  } catch {
14175
14203
  keytarCache = null;
14176
- return null;
14177
14204
  }
14205
+ return keytarCache;
14178
14206
  }
14179
14207
  async function loadKeytarOrThrow() {
14180
14208
  const k = await loadKeytar();
14181
14209
  if (k === null) {
14182
14210
  throw new KeytarUnavailableError(
14183
- "failed to load `keytar` native module. On Debian/Ubuntu install `libsecret-1-0`; on Fedora install `gnome-keyring`; on macOS/Windows this should never happen \u2014 file a bug."
14211
+ "no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
14184
14212
  );
14185
14213
  }
14186
14214
  return k;
package/dist/index.d.cts CHANGED
@@ -882,7 +882,7 @@ declare function resolveStateDir(opts?: ResolveOptions): PathResolution;
882
882
  *
883
883
  * Priority chain (per `shared/contracts/seal_mcp.v1.json::passphrase_delivery`):
884
884
  *
885
- * 1. OS keychain via `keytar` (service `'seal-mcp'`, account `'state'`).
885
+ * 1. OS keychain via `@napi-rs/keyring` (service `'seal-mcp'`, account `'state'`).
886
886
  * 2. `SEAL_MCP_PASSPHRASE` environment variable. Logs a warning to
887
887
  * stderr because env vars are visible via `/proc/<pid>/environ`
888
888
  * on Linux, `ps -E`/`procstat -e` on BSD, and via the process
@@ -890,10 +890,9 @@ declare function resolveStateDir(opts?: ResolveOptions): PathResolution;
890
890
  * 3. Cleartext on disk — REFUSED. Listed only to document the
891
891
  * refusal; the resolver has no codepath that reads from a file.
892
892
  *
893
- * `keytar` is loaded lazily (same pattern as `clipboardy` in
894
- * `handoff.ts`) so headless environments lacking `libsecret-1.so.0`
895
- * (Linux) can still fall back to the env var without crashing the CLI
896
- * at import time.
893
+ * The keychain module is an optional dependency loaded lazily (same pattern
894
+ * as `clipboardy` in `handoff.ts`), so a machine without it, or without a
895
+ * keychain, falls back to the env var without crashing at import time.
897
896
  */
898
897
  declare const KEYCHAIN_SERVICE: "seal-mcp";
899
898
  declare const KEYCHAIN_ACCOUNT: "state";
@@ -904,8 +903,8 @@ interface ResolvedPassphrase {
904
903
  readonly source: PassphraseSource;
905
904
  }
906
905
  /**
907
- * Surface used by `passphrase.ts` — kept narrow on purpose so tests can
908
- * stub keytar without pulling in the native module.
906
+ * Surface used by `passphrase.ts` — `keytar`'s shape, kept narrow on purpose
907
+ * so tests can stub the keychain without pulling in the native module.
909
908
  */
910
909
  interface KeytarLike {
911
910
  getPassword(service: string, account: string): Promise<string | null>;
package/dist/index.d.ts CHANGED
@@ -882,7 +882,7 @@ declare function resolveStateDir(opts?: ResolveOptions): PathResolution;
882
882
  *
883
883
  * Priority chain (per `shared/contracts/seal_mcp.v1.json::passphrase_delivery`):
884
884
  *
885
- * 1. OS keychain via `keytar` (service `'seal-mcp'`, account `'state'`).
885
+ * 1. OS keychain via `@napi-rs/keyring` (service `'seal-mcp'`, account `'state'`).
886
886
  * 2. `SEAL_MCP_PASSPHRASE` environment variable. Logs a warning to
887
887
  * stderr because env vars are visible via `/proc/<pid>/environ`
888
888
  * on Linux, `ps -E`/`procstat -e` on BSD, and via the process
@@ -890,10 +890,9 @@ declare function resolveStateDir(opts?: ResolveOptions): PathResolution;
890
890
  * 3. Cleartext on disk — REFUSED. Listed only to document the
891
891
  * refusal; the resolver has no codepath that reads from a file.
892
892
  *
893
- * `keytar` is loaded lazily (same pattern as `clipboardy` in
894
- * `handoff.ts`) so headless environments lacking `libsecret-1.so.0`
895
- * (Linux) can still fall back to the env var without crashing the CLI
896
- * at import time.
893
+ * The keychain module is an optional dependency loaded lazily (same pattern
894
+ * as `clipboardy` in `handoff.ts`), so a machine without it, or without a
895
+ * keychain, falls back to the env var without crashing at import time.
897
896
  */
898
897
  declare const KEYCHAIN_SERVICE: "seal-mcp";
899
898
  declare const KEYCHAIN_ACCOUNT: "state";
@@ -904,8 +903,8 @@ interface ResolvedPassphrase {
904
903
  readonly source: PassphraseSource;
905
904
  }
906
905
  /**
907
- * Surface used by `passphrase.ts` — kept narrow on purpose so tests can
908
- * stub keytar without pulling in the native module.
906
+ * Surface used by `passphrase.ts` — `keytar`'s shape, kept narrow on purpose
907
+ * so tests can stub the keychain without pulling in the native module.
909
908
  */
910
909
  interface KeytarLike {
911
910
  getPassword(service: string, account: string): Promise<string | null>;
package/dist/index.js CHANGED
@@ -4520,7 +4520,7 @@ var seal_mcp_v1_default = {
4520
4520
  {
4521
4521
  rank: 1,
4522
4522
  method: "os_keychain",
4523
- implementation: "keytar (libsecret on Linux, macOS Keychain, Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
4523
+ implementation: "@napi-rs/keyring (the Secret Service on Linux, never the kernel keyring, which forgets on reboot; macOS Keychain; Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
4524
4524
  default: true
4525
4525
  },
4526
4526
  {
@@ -13720,7 +13720,7 @@ function proFileGetOutputToWire(out) {
13720
13720
  }
13721
13721
 
13722
13722
  // src/server.ts
13723
- var SERVER_VERSION = "0.2.3";
13723
+ var SERVER_VERSION = "0.2.4";
13724
13724
  var toolMeta = (name) => {
13725
13725
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
13726
13726
  return { title, description, annotations };
@@ -14069,6 +14069,7 @@ function osDefaultDir(env, platform, home) {
14069
14069
  }
14070
14070
 
14071
14071
  // src/passphrase.ts
14072
+ import { execFile as execFile2 } from "child_process";
14072
14073
  import { randomBytes as randomBytes4 } from "crypto";
14073
14074
  var SERVICE_NAME = "seal-mcp";
14074
14075
  var ACCOUNT_NAME = "state";
@@ -14092,23 +14093,50 @@ var PassphraseNotFoundError = class extends Error {
14092
14093
  this.name = "PassphraseNotFoundError";
14093
14094
  }
14094
14095
  };
14096
+ function keyringEntry(keyring, platform, service, account) {
14097
+ return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
14098
+ }
14099
+ function readKeytarLinuxEntry(service, account) {
14100
+ return new Promise((resolve5) => {
14101
+ execFile2("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
14102
+ const value = err2 ? "" : stdout.replace(/\n$/, "");
14103
+ resolve5(value.length > 0 ? value : null);
14104
+ });
14105
+ });
14106
+ }
14107
+ function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
14108
+ const entry = (service, account) => keyringEntry(keyring, platform, service, account);
14109
+ const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
14110
+ return {
14111
+ async getPassword(service, account) {
14112
+ const secret = await entry(service, account).getSecret();
14113
+ if (secret !== void 0) return Buffer.from(secret).toString("utf8");
14114
+ const legacy = platform === "linux" ? await readLegacy(service, account) : null;
14115
+ if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
14116
+ return legacy;
14117
+ },
14118
+ setPassword: store,
14119
+ deletePassword: (service, account) => entry(service, account).deleteCredential()
14120
+ };
14121
+ }
14095
14122
  var keytarCache = void 0;
14096
14123
  async function loadKeytar() {
14097
14124
  if (keytarCache !== void 0) return keytarCache;
14098
14125
  try {
14099
- const mod = await import("keytar");
14100
- keytarCache = mod.default ?? mod;
14101
- return keytarCache;
14126
+ const mod = await import("@napi-rs/keyring");
14127
+ const keyring = mod.default ?? mod;
14128
+ keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
14129
+ keytarCache = keytarOverKeyring(keyring);
14102
14130
  } catch {
14103
14131
  keytarCache = null;
14104
- return null;
14105
14132
  }
14133
+ return keytarCache;
14106
14134
  }
14107
14135
  async function loadKeytarOrThrow() {
14108
14136
  const k = await loadKeytar();
14109
14137
  if (k === null) {
14110
14138
  throw new KeytarUnavailableError(
14111
- "failed to load `keytar` native module. On Debian/Ubuntu install `libsecret-1-0`; on Fedora install `gnome-keyring`; on macOS/Windows this should never happen \u2014 file a bug."
14139
+ "no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
14112
14140
  );
14113
14141
  }
14114
14142
  return k;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sealnet-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "mcpName": "net.seal/mcp",
5
5
  "description": "Model Context Protocol server for SEAL — model never holds capability by default (handoff mode); explicit opt-in to forward mode with strict TTL/recipient policy.",
6
6
  "type": "module",
@@ -59,7 +59,7 @@
59
59
  "commander": "^14.0.3"
60
60
  },
61
61
  "optionalDependencies": {
62
- "keytar": "^7.9.0"
62
+ "@napi-rs/keyring": "^2.1.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/node": "^20.10.0",