sealnet-mcp 0.2.4 → 0.3.0

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/dist/cli.cjs CHANGED
@@ -34,8 +34,9 @@ __export(cli_exports, {
34
34
  main: () => main
35
35
  });
36
36
  module.exports = __toCommonJS(cli_exports);
37
- var import_node_fs6 = require("fs");
37
+ var import_node_fs7 = require("fs");
38
38
  var import_promises10 = require("fs/promises");
39
+ var import_node_path11 = require("path");
39
40
  var import_commander = require("commander");
40
41
 
41
42
  // ../../../node_modules/.pnpm/@noble+hashes@1.8.0/node_modules/@noble/hashes/esm/utils.js
@@ -6901,6 +6902,9 @@ async function generateIdentity() {
6901
6902
  const { publicKey, secretKey } = await generateRecipientKeyPair();
6902
6903
  return { secret: secretKey, public: publicKey };
6903
6904
  }
6905
+ async function identityFromSeed(seed) {
6906
+ return { secret: seed, public: await recipientPublicKey(seed) };
6907
+ }
6904
6908
  function identityToFields(id) {
6905
6909
  return {
6906
6910
  x25519_secret_b64: toB64(id.secret),
@@ -6934,20 +6938,27 @@ var import_node_crypto = require("crypto");
6934
6938
  var import_node_os = require("os");
6935
6939
  var import_node_path = require("path");
6936
6940
  var LOCK_FILE_NAME = "state.lock";
6941
+ var LOCK_WAIT_MS = 5e3;
6942
+ var LOCK_RETRY_MS = 25;
6937
6943
  var StateLockedError = class extends Error {
6938
6944
  code = "state_locked";
6939
6945
  holder;
6940
6946
  lockPath;
6941
6947
  constructor(lockPath, holder) {
6942
6948
  const who = holder === null ? "holder info unreadable" : `pid ${holder.pid} on ${holder.hostname} since ${holder.acquired_at}`;
6943
- super(
6944
- `state directory is locked by another sealnet-mcp process (${who}). Only one writer may use a state dir at a time; stop the other instance or point this one at a different --state-dir. Lock file: ${lockPath}`
6945
- );
6949
+ super(`state directory is locked by another sealnet-mcp process (${who}). Lock file: ${lockPath}`);
6946
6950
  this.name = "StateLockedError";
6947
6951
  this.holder = holder;
6948
6952
  this.lockPath = lockPath;
6949
6953
  }
6950
6954
  };
6955
+ var StateBusyError = class extends Error {
6956
+ code = "state_busy";
6957
+ constructor(locked) {
6958
+ super(`${locked.message}; waited ${LOCK_WAIT_MS / 1e3} s, nothing was changed, safe to retry`);
6959
+ this.name = "StateBusyError";
6960
+ }
6961
+ };
6951
6962
  var StateDirUnsafeError = class extends Error {
6952
6963
  code = "state_dir_unsafe";
6953
6964
  constructor(stateDir, reason) {
@@ -6966,6 +6977,9 @@ function defaultIsPidAlive(pid) {
6966
6977
  return code === "EPERM";
6967
6978
  }
6968
6979
  }
6980
+ function holderMayRun(holder, deps = {}) {
6981
+ return holder.hostname !== (deps.host ?? (0, import_node_os.hostname)()) || (deps.isPidAlive ?? defaultIsPidAlive)(holder.pid);
6982
+ }
6969
6983
  function parseHolder(raw) {
6970
6984
  try {
6971
6985
  const parsed = JSON.parse(raw);
@@ -7008,7 +7022,7 @@ async function tryCreate(lockPath, info) {
7008
7022
  await (0, import_promises.unlink)(tmp).catch(() => void 0);
7009
7023
  }
7010
7024
  }
7011
- async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
7025
+ async function tryBreakStale(lockPath, probed, deps) {
7012
7026
  const claimPath = `${lockPath}.break-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}`;
7013
7027
  try {
7014
7028
  await (0, import_promises.rename)(lockPath, claimPath);
@@ -7016,8 +7030,7 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
7016
7030
  return true;
7017
7031
  }
7018
7032
  const claimed = parseHolder(await (0, import_promises.readFile)(claimPath, "utf8").catch(() => ""));
7019
- const claimedStale = claimed === null || claimed.hostname === selfHost && !isAlive(claimed.pid);
7020
- if (sameHolder(claimed, probed) || claimedStale) {
7033
+ if (sameHolder(claimed, probed) || claimed === null || !holderMayRun(claimed, deps)) {
7021
7034
  await (0, import_promises.unlink)(claimPath).catch(() => void 0);
7022
7035
  return true;
7023
7036
  }
@@ -7030,11 +7043,9 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
7030
7043
  }
7031
7044
  async function acquireStateLock(stateDir, deps = {}) {
7032
7045
  const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
7033
- const isAlive = deps.isPidAlive ?? defaultIsPidAlive;
7034
- const selfHost = deps.host ?? (0, import_node_os.hostname)();
7035
7046
  const self2 = {
7036
7047
  pid: deps.pid ?? process.pid,
7037
- hostname: selfHost,
7048
+ hostname: deps.host ?? (0, import_node_os.hostname)(),
7038
7049
  acquired_at: (/* @__PURE__ */ new Date()).toISOString()
7039
7050
  };
7040
7051
  await (0, import_promises.mkdir)(stateDir, { recursive: true, mode: 448 });
@@ -7045,11 +7056,10 @@ async function acquireStateLock(stateDir, deps = {}) {
7045
7056
  await (0, import_promises.chmod)(stateDir, 448).catch(() => void 0);
7046
7057
  if (!await tryCreate(lockPath, self2)) {
7047
7058
  const holder = await readHolder(lockPath);
7048
- const stale = holder !== null && holder.hostname === selfHost && !isAlive(holder.pid);
7049
- if (holder !== null && !stale) {
7059
+ if (holder !== null && holderMayRun(holder, deps)) {
7050
7060
  throw new StateLockedError(lockPath, holder);
7051
7061
  }
7052
- if (!await tryBreakStale(lockPath, holder, isAlive, selfHost)) {
7062
+ if (!await tryBreakStale(lockPath, holder, deps)) {
7053
7063
  throw new StateLockedError(lockPath, await readHolder(lockPath));
7054
7064
  }
7055
7065
  if (!await tryCreate(lockPath, self2)) {
@@ -7081,6 +7091,30 @@ async function acquireStateLock(stateDir, deps = {}) {
7081
7091
  }
7082
7092
  };
7083
7093
  }
7094
+ var queues = /* @__PURE__ */ new Map();
7095
+ function withStateLock(stateDir, fn, deps = {}) {
7096
+ const run = (queues.get(stateDir) ?? Promise.resolve()).catch(() => void 0).then(async () => {
7097
+ const deadline = Date.now() + LOCK_WAIT_MS;
7098
+ let lock;
7099
+ for (; ; ) {
7100
+ try {
7101
+ lock = await acquireStateLock(stateDir, deps);
7102
+ break;
7103
+ } catch (err2) {
7104
+ if (!(err2 instanceof StateLockedError)) throw err2;
7105
+ if (Date.now() >= deadline) throw new StateBusyError(err2);
7106
+ await new Promise((resolve5) => setTimeout(resolve5, LOCK_RETRY_MS));
7107
+ }
7108
+ }
7109
+ try {
7110
+ return await fn();
7111
+ } finally {
7112
+ await lock.release();
7113
+ }
7114
+ });
7115
+ queues.set(stateDir, run);
7116
+ return run;
7117
+ }
7084
7118
  async function probeStateLock(stateDir, deps = {}) {
7085
7119
  const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
7086
7120
  const holder = await readHolder(lockPath);
@@ -7092,182 +7126,7 @@ async function probeStateLock(stateDir, deps = {}) {
7092
7126
  return { held: false, holder: null, stale: false };
7093
7127
  }
7094
7128
  }
7095
- const isAlive = deps.isPidAlive ?? defaultIsPidAlive;
7096
- const sameHost = holder.hostname === (deps.host ?? (0, import_node_os.hostname)());
7097
- const stale = sameHost && !isAlive(holder.pid);
7098
- return { held: true, holder, stale };
7099
- }
7100
-
7101
- // src/passphrase.ts
7102
- var import_node_child_process = require("child_process");
7103
- var import_node_crypto2 = require("crypto");
7104
- var SERVICE_NAME = "seal-mcp";
7105
- var ACCOUNT_NAME = "state";
7106
- var ENV_VAR = "SEAL_MCP_PASSPHRASE";
7107
- var KEYCHAIN_SERVICE = SERVICE_NAME;
7108
- var KEYCHAIN_ACCOUNT = ACCOUNT_NAME;
7109
- var PASSPHRASE_ENV_VAR = ENV_VAR;
7110
- var KeytarUnavailableError = class extends Error {
7111
- code = "keytar_unavailable";
7112
- constructor(detail) {
7113
- super(`keytar unavailable: ${detail}`);
7114
- this.name = "KeytarUnavailableError";
7115
- }
7116
- };
7117
- var PassphraseNotFoundError = class extends Error {
7118
- code = "passphrase_not_found";
7119
- constructor() {
7120
- super(
7121
- `No passphrase found. Run \`sealnet-mcp init\` to store one in your OS keychain, or export ${ENV_VAR} for CI/headless use.`
7122
- );
7123
- this.name = "PassphraseNotFoundError";
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
- }
7152
- var keytarCache = void 0;
7153
- async function loadKeytar() {
7154
- if (keytarCache !== void 0) return keytarCache;
7155
- try {
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);
7160
- } catch {
7161
- keytarCache = null;
7162
- }
7163
- return keytarCache;
7164
- }
7165
- async function loadKeytarOrThrow() {
7166
- const k = await loadKeytar();
7167
- if (k === null) {
7168
- throw new KeytarUnavailableError(
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."
7170
- );
7171
- }
7172
- return k;
7173
- }
7174
- async function resolvePassphraseFromStores(env = process.env) {
7175
- const keytar = await loadKeytar();
7176
- if (keytar !== null) {
7177
- try {
7178
- const stored = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
7179
- if (stored !== null && stored.length > 0) {
7180
- return { passphrase: stored, source: "keychain" };
7181
- }
7182
- } catch {
7183
- }
7184
- }
7185
- const fromEnv = env[ENV_VAR];
7186
- if (fromEnv !== void 0 && fromEnv.length > 0) {
7187
- return { passphrase: fromEnv, source: "env" };
7188
- }
7189
- return null;
7190
- }
7191
- async function passphraseForNewState(env = process.env) {
7192
- const stored = await resolvePassphraseFromStores(env);
7193
- if (stored !== null) return { ...stored, fresh: false };
7194
- if (!(await probeKeychain()).available) return null;
7195
- return { passphrase: (0, import_node_crypto2.randomBytes)(32).toString("base64url"), source: "keychain", fresh: true };
7196
- }
7197
- async function probeKeychain() {
7198
- const keytar = await loadKeytar();
7199
- if (keytar === null) return { available: false, hasEntry: false };
7200
- try {
7201
- const v = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
7202
- return { available: true, hasEntry: v !== null && v.length > 0 };
7203
- } catch {
7204
- return { available: false, hasEntry: false };
7205
- }
7206
- }
7207
- async function setKeychainPassphrase(passphrase) {
7208
- if (passphrase.length === 0) {
7209
- throw new Error("refusing to store an empty passphrase in the keychain");
7210
- }
7211
- const keytar = await loadKeytarOrThrow();
7212
- await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, passphrase);
7213
- }
7214
- async function deleteKeychainPassphrase() {
7215
- const keytar = await loadKeytar();
7216
- if (keytar === null) return false;
7217
- try {
7218
- return await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
7219
- } catch {
7220
- return false;
7221
- }
7222
- }
7223
- async function promptHiddenPassphrase(prompt) {
7224
- const stdin = process.stdin;
7225
- if (!stdin.isTTY) {
7226
- throw new Error(
7227
- `Cannot prompt for passphrase: stdin is not a TTY. Set ${ENV_VAR} for non-interactive use.`
7228
- );
7229
- }
7230
- process.stdout.write(prompt);
7231
- const wasRaw = stdin.isRaw;
7232
- stdin.setRawMode(true);
7233
- stdin.resume();
7234
- stdin.setEncoding("utf8");
7235
- return new Promise((resolve5, reject) => {
7236
- let buf = "";
7237
- const cleanup = () => {
7238
- stdin.removeListener("data", onData);
7239
- stdin.setRawMode(wasRaw);
7240
- stdin.pause();
7241
- process.stdout.write("\n");
7242
- };
7243
- const onData = (chunk) => {
7244
- const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
7245
- for (let i = 0; i < text.length; i++) {
7246
- const ch = text[i];
7247
- if (ch === "") {
7248
- cleanup();
7249
- reject(new Error("cancelled"));
7250
- return;
7251
- }
7252
- if (ch === "" && buf.length === 0) {
7253
- cleanup();
7254
- reject(new Error("cancelled"));
7255
- return;
7256
- }
7257
- if (ch === "\r" || ch === "\n") {
7258
- cleanup();
7259
- resolve5(buf);
7260
- return;
7261
- }
7262
- if (ch === "\x7F" || ch === "\b") {
7263
- buf = buf.slice(0, -1);
7264
- continue;
7265
- }
7266
- buf += ch;
7267
- }
7268
- };
7269
- stdin.on("data", onData);
7270
- });
7129
+ return { held: true, holder, stale: !holderMayRun(holder, deps) };
7271
7130
  }
7272
7131
 
7273
7132
  // src/paths.ts
@@ -7310,148 +7169,582 @@ function osDefaultDir(env, platform, home) {
7310
7169
  }
7311
7170
  }
7312
7171
 
7313
- // src/server.ts
7314
- var import_node_path10 = require("path");
7315
- var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
7316
- var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7317
-
7318
- // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
7319
- var external_exports = {};
7320
- __export(external_exports, {
7321
- BRAND: () => BRAND,
7322
- DIRTY: () => DIRTY,
7323
- EMPTY_PATH: () => EMPTY_PATH,
7324
- INVALID: () => INVALID,
7325
- NEVER: () => NEVER,
7326
- OK: () => OK,
7327
- ParseStatus: () => ParseStatus,
7328
- Schema: () => ZodType,
7329
- ZodAny: () => ZodAny,
7330
- ZodArray: () => ZodArray,
7331
- ZodBigInt: () => ZodBigInt,
7332
- ZodBoolean: () => ZodBoolean,
7333
- ZodBranded: () => ZodBranded,
7334
- ZodCatch: () => ZodCatch,
7335
- ZodDate: () => ZodDate,
7336
- ZodDefault: () => ZodDefault,
7337
- ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
7338
- ZodEffects: () => ZodEffects,
7339
- ZodEnum: () => ZodEnum,
7340
- ZodError: () => ZodError,
7341
- ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
7342
- ZodFunction: () => ZodFunction,
7343
- ZodIntersection: () => ZodIntersection,
7344
- ZodIssueCode: () => ZodIssueCode,
7345
- ZodLazy: () => ZodLazy,
7346
- ZodLiteral: () => ZodLiteral,
7347
- ZodMap: () => ZodMap,
7348
- ZodNaN: () => ZodNaN,
7349
- ZodNativeEnum: () => ZodNativeEnum,
7350
- ZodNever: () => ZodNever,
7351
- ZodNull: () => ZodNull,
7352
- ZodNullable: () => ZodNullable,
7353
- ZodNumber: () => ZodNumber,
7354
- ZodObject: () => ZodObject,
7355
- ZodOptional: () => ZodOptional,
7356
- ZodParsedType: () => ZodParsedType,
7357
- ZodPipeline: () => ZodPipeline,
7358
- ZodPromise: () => ZodPromise,
7359
- ZodReadonly: () => ZodReadonly,
7360
- ZodRecord: () => ZodRecord,
7361
- ZodSchema: () => ZodType,
7362
- ZodSet: () => ZodSet,
7363
- ZodString: () => ZodString,
7364
- ZodSymbol: () => ZodSymbol,
7365
- ZodTransformer: () => ZodEffects,
7366
- ZodTuple: () => ZodTuple,
7367
- ZodType: () => ZodType,
7368
- ZodUndefined: () => ZodUndefined,
7369
- ZodUnion: () => ZodUnion,
7370
- ZodUnknown: () => ZodUnknown,
7371
- ZodVoid: () => ZodVoid,
7372
- addIssueToContext: () => addIssueToContext,
7373
- any: () => anyType,
7374
- array: () => arrayType,
7375
- bigint: () => bigIntType,
7376
- boolean: () => booleanType,
7377
- coerce: () => coerce,
7378
- custom: () => custom,
7379
- date: () => dateType,
7380
- datetimeRegex: () => datetimeRegex,
7381
- defaultErrorMap: () => en_default,
7382
- discriminatedUnion: () => discriminatedUnionType,
7383
- effect: () => effectsType,
7384
- enum: () => enumType,
7385
- function: () => functionType,
7386
- getErrorMap: () => getErrorMap,
7387
- getParsedType: () => getParsedType,
7388
- instanceof: () => instanceOfType,
7389
- intersection: () => intersectionType,
7390
- isAborted: () => isAborted,
7391
- isAsync: () => isAsync,
7392
- isDirty: () => isDirty,
7393
- isValid: () => isValid,
7394
- late: () => late,
7395
- lazy: () => lazyType,
7396
- literal: () => literalType,
7397
- makeIssue: () => makeIssue,
7398
- map: () => mapType,
7399
- nan: () => nanType,
7400
- nativeEnum: () => nativeEnumType,
7401
- never: () => neverType,
7402
- null: () => nullType,
7403
- nullable: () => nullableType,
7404
- number: () => numberType,
7405
- object: () => objectType,
7406
- objectUtil: () => objectUtil,
7407
- oboolean: () => oboolean,
7408
- onumber: () => onumber,
7409
- optional: () => optionalType,
7410
- ostring: () => ostring,
7411
- pipeline: () => pipelineType,
7412
- preprocess: () => preprocessType,
7413
- promise: () => promiseType,
7414
- quotelessJson: () => quotelessJson,
7415
- record: () => recordType,
7416
- set: () => setType,
7417
- setErrorMap: () => setErrorMap,
7418
- strictObject: () => strictObjectType,
7419
- string: () => stringType,
7420
- symbol: () => symbolType,
7421
- transformer: () => effectsType,
7422
- tuple: () => tupleType,
7423
- undefined: () => undefinedType,
7424
- union: () => unionType,
7425
- unknown: () => unknownType,
7426
- util: () => util,
7427
- void: () => voidType
7428
- });
7172
+ // src/seed.ts
7173
+ var import_node_child_process = require("child_process");
7174
+ var import_node_crypto3 = require("crypto");
7175
+ var import_node_fs3 = require("fs");
7176
+ var import_node_path4 = require("path");
7429
7177
 
7430
- // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
7431
- var util;
7432
- (function(util2) {
7433
- util2.assertEqual = (_) => {
7434
- };
7435
- function assertIs(_arg) {
7436
- }
7437
- util2.assertIs = assertIs;
7438
- function assertNever(_x) {
7439
- throw new Error();
7440
- }
7441
- util2.assertNever = assertNever;
7442
- util2.arrayToEnum = (items) => {
7443
- const obj = {};
7444
- for (const item of items) {
7445
- obj[item] = item;
7446
- }
7447
- return obj;
7448
- };
7449
- util2.getValidEnumValues = (obj) => {
7450
- const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
7451
- const filtered = {};
7452
- for (const k of validKeys) {
7453
- filtered[k] = obj[k];
7454
- }
7178
+ // src/state.ts
7179
+ var import_node_crypto2 = require("crypto");
7180
+ var import_node_fs2 = require("fs");
7181
+ var import_node_path3 = require("path");
7182
+ var import_argon2 = __toESM(require("argon2"), 1);
7183
+ var FORMAT_TAG = "SEAL_MCP_STATE v1";
7184
+ var STATE_VERSION = 1;
7185
+ var ARGON2_MEMORY_KIB = 65536;
7186
+ var ARGON2_ITERATIONS = 3;
7187
+ var ARGON2_PARALLELISM = 4;
7188
+ var ARGON2_SALT_BYTES = 16;
7189
+ var KDF_BOUNDS = {
7190
+ memory_kib: { min: 8192, max: 1048576 },
7191
+ // 8 MiB .. 1 GiB
7192
+ iterations: { min: 1, max: 16 },
7193
+ parallelism: { min: 1, max: 16 }
7194
+ };
7195
+ var AES_KEY_BYTES = 32;
7196
+ var AES_NONCE_BYTES = 12;
7197
+ var StatePassphraseInvalid = class extends Error {
7198
+ constructor() {
7199
+ super("AES-256-GCM tag check failed: another passphrase, or a tampered file");
7200
+ this.name = "StatePassphraseInvalid";
7201
+ }
7202
+ };
7203
+ var StateCorrupt = class extends Error {
7204
+ constructor(reason) {
7205
+ super(`state.corrupt: ${reason}`);
7206
+ this.name = "StateCorrupt";
7207
+ }
7208
+ };
7209
+ function toB642(bytes) {
7210
+ return Buffer.from(bytes).toString("base64");
7211
+ }
7212
+ function fromB642(s) {
7213
+ return new Uint8Array(Buffer.from(s, "base64"));
7214
+ }
7215
+ var CURRENT_KDF_PARAMS = {
7216
+ memory_kib: ARGON2_MEMORY_KIB,
7217
+ iterations: ARGON2_ITERATIONS,
7218
+ parallelism: ARGON2_PARALLELISM
7219
+ };
7220
+ function boundedKdfParam(name, value) {
7221
+ if (typeof value !== "number" || !Number.isInteger(value)) {
7222
+ throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
7223
+ }
7224
+ const { min, max } = KDF_BOUNDS[name];
7225
+ if (value < min || value > max) {
7226
+ throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
7227
+ }
7228
+ return value;
7229
+ }
7230
+ async function deriveKek(passphrase, saltB64, params) {
7231
+ const salt = fromB642(saltB64);
7232
+ if (salt.length !== ARGON2_SALT_BYTES) {
7233
+ throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
7234
+ }
7235
+ const kek = await import_argon2.default.hash(passphrase, {
7236
+ type: import_argon2.default.argon2id,
7237
+ raw: true,
7238
+ salt: Buffer.from(salt),
7239
+ memoryCost: params.memory_kib,
7240
+ timeCost: params.iterations,
7241
+ parallelism: params.parallelism,
7242
+ hashLength: AES_KEY_BYTES
7243
+ });
7244
+ return new Uint8Array(kek);
7245
+ }
7246
+ async function aesGcmEncrypt(kek, nonce, plaintext) {
7247
+ const key = await import_node_crypto2.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
7248
+ const ct = await import_node_crypto2.webcrypto.subtle.encrypt(
7249
+ { name: "AES-GCM", iv: nonce, tagLength: 128 },
7250
+ key,
7251
+ plaintext
7252
+ );
7253
+ return new Uint8Array(ct);
7254
+ }
7255
+ async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
7256
+ const key = await import_node_crypto2.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
7257
+ try {
7258
+ const pt = await import_node_crypto2.webcrypto.subtle.decrypt(
7259
+ { name: "AES-GCM", iv: nonce, tagLength: 128 },
7260
+ key,
7261
+ ciphertextWithTag
7262
+ );
7263
+ return new Uint8Array(pt);
7264
+ } catch {
7265
+ throw new StatePassphraseInvalid();
7266
+ }
7267
+ }
7268
+ function createEmptyState(identity) {
7269
+ return {
7270
+ version: STATE_VERSION,
7271
+ identity,
7272
+ handles: {},
7273
+ audit: [],
7274
+ pending: {}
7275
+ };
7276
+ }
7277
+ function validateEnvelope(json) {
7278
+ if (typeof json !== "object" || json === null) {
7279
+ throw new StateCorrupt("envelope is not an object");
7280
+ }
7281
+ const e = json;
7282
+ if (e.version !== STATE_VERSION) {
7283
+ throw new StateCorrupt(`unknown version: ${String(e.version)}`);
7284
+ }
7285
+ if (e.format !== FORMAT_TAG) {
7286
+ throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
7287
+ }
7288
+ const kdf = e.kdf;
7289
+ if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
7290
+ throw new StateCorrupt("kdf header missing or wrong algo");
7291
+ }
7292
+ const cipher = e.cipher;
7293
+ if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
7294
+ throw new StateCorrupt("cipher header missing or wrong algo");
7295
+ }
7296
+ }
7297
+ function validatePlaintext(json) {
7298
+ if (typeof json !== "object" || json === null) {
7299
+ throw new StateCorrupt("plaintext is not an object");
7300
+ }
7301
+ const s = json;
7302
+ if (s.version !== STATE_VERSION) {
7303
+ throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
7304
+ }
7305
+ const id = s.identity;
7306
+ if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
7307
+ throw new StateCorrupt("identity missing or malformed");
7308
+ }
7309
+ if (typeof s.handles !== "object" || s.handles === null) {
7310
+ throw new StateCorrupt("handles is not an object");
7311
+ }
7312
+ if (!Array.isArray(s.audit)) {
7313
+ throw new StateCorrupt("audit is not an array");
7314
+ }
7315
+ if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
7316
+ throw new StateCorrupt("pending is not an object");
7317
+ }
7318
+ }
7319
+ async function loadState(passphrase, statePath) {
7320
+ const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
7321
+ let envelope;
7322
+ try {
7323
+ envelope = JSON.parse(raw);
7324
+ } catch (err2) {
7325
+ throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
7326
+ }
7327
+ validateEnvelope(envelope);
7328
+ const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
7329
+ memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
7330
+ iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
7331
+ parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
7332
+ });
7333
+ const nonce = fromB642(envelope.cipher.nonce_b64);
7334
+ if (nonce.length !== AES_NONCE_BYTES) {
7335
+ throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
7336
+ }
7337
+ const ct = fromB642(envelope.cipher.ciphertext_b64);
7338
+ const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
7339
+ const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
7340
+ let plaintext;
7341
+ try {
7342
+ plaintext = JSON.parse(ptText);
7343
+ } catch (err2) {
7344
+ throw new StateCorrupt(`plaintext is not valid JSON: ${err2.message}`);
7345
+ }
7346
+ validatePlaintext(plaintext);
7347
+ if (plaintext.pending === void 0) {
7348
+ plaintext.pending = {};
7349
+ }
7350
+ return plaintext;
7351
+ }
7352
+ async function saveState(state, passphrase, statePath) {
7353
+ validatePlaintext(state);
7354
+ const salt = (0, import_node_crypto2.randomBytes)(ARGON2_SALT_BYTES);
7355
+ const nonce = (0, import_node_crypto2.randomBytes)(AES_NONCE_BYTES);
7356
+ const kek = await deriveKek(passphrase, toB642(salt), CURRENT_KDF_PARAMS);
7357
+ const ptBytes = new TextEncoder().encode(JSON.stringify(state));
7358
+ const ct = await aesGcmEncrypt(kek, nonce, ptBytes);
7359
+ const envelope = {
7360
+ version: STATE_VERSION,
7361
+ format: FORMAT_TAG,
7362
+ kdf: {
7363
+ algo: "argon2id",
7364
+ memory_kib: ARGON2_MEMORY_KIB,
7365
+ iterations: ARGON2_ITERATIONS,
7366
+ parallelism: ARGON2_PARALLELISM,
7367
+ salt_b64: toB642(salt)
7368
+ },
7369
+ cipher: {
7370
+ algo: "aes-256-gcm",
7371
+ nonce_b64: toB642(nonce),
7372
+ ciphertext_b64: toB642(ct),
7373
+ tag_included: true
7374
+ }
7375
+ };
7376
+ const dir = (0, import_node_path3.dirname)(statePath);
7377
+ await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
7378
+ const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
7379
+ const fh = await import_node_fs2.promises.open(tmp, "w", 384);
7380
+ try {
7381
+ await fh.writeFile(JSON.stringify(envelope), "utf8");
7382
+ await fh.sync();
7383
+ } finally {
7384
+ await fh.close();
7385
+ }
7386
+ await import_node_fs2.promises.rename(tmp, statePath);
7387
+ try {
7388
+ const dh = await import_node_fs2.promises.open(dir, "r");
7389
+ try {
7390
+ await dh.sync();
7391
+ } finally {
7392
+ await dh.close();
7393
+ }
7394
+ } catch {
7395
+ }
7396
+ await import_node_fs2.promises.chmod(dir, 448).catch(() => {
7397
+ });
7398
+ }
7399
+ function updateState(passphrase, statePath, mutate) {
7400
+ return withStateLock((0, import_node_path3.dirname)(statePath), async () => {
7401
+ const view = await loadState(passphrase, statePath);
7402
+ if (await mutate(view) !== false) await saveState(view, passphrase, statePath);
7403
+ return view;
7404
+ });
7405
+ }
7406
+
7407
+ // src/seed.ts
7408
+ var SEED_ENV_VAR = "SEAL_SEED";
7409
+ var EPHEMERAL_ENV_VAR = "SEAL_MCP_EPHEMERAL";
7410
+ var PASSPHRASE_ENV_VAR = "SEAL_MCP_PASSPHRASE";
7411
+ var SEED_FILE_NAME = "seed";
7412
+ var KEYCHAIN_SERVICE = "seal-mcp";
7413
+ var KEYCHAIN_ACCOUNT = "seed";
7414
+ var LEGACY_KEYCHAIN_ACCOUNT = "state";
7415
+ var SeedInvalidError = class extends Error {
7416
+ code = "seed_invalid";
7417
+ constructor(where) {
7418
+ super(
7419
+ `${where} is not an agent key: expected 43 base64url characters, as \`sealnet-mcp seed --force\` or \`seal keygen --export --force\` print them`
7420
+ );
7421
+ this.name = "SeedInvalidError";
7422
+ }
7423
+ };
7424
+ var IdentityConflictError = class extends Error {
7425
+ code = "identity_conflict";
7426
+ constructor(statePath) {
7427
+ super(
7428
+ `${statePath} does not open with this agent's key: it belongs to another key (a foreign ${SEED_ENV_VAR}?) or was tampered with. Unset ${SEED_ENV_VAR}, or run \`sealnet-mcp reset --force\` to start over with a new address.`
7429
+ );
7430
+ this.name = "IdentityConflictError";
7431
+ }
7432
+ };
7433
+ function keyringEntry(keyring, platform, service, account) {
7434
+ return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
7435
+ }
7436
+ function readKeytarLinuxEntry(service, account) {
7437
+ return new Promise((resolve5) => {
7438
+ (0, import_node_child_process.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
7439
+ const value = err2 ? "" : stdout.replace(/\n$/, "");
7440
+ resolve5(value.length > 0 ? value : null);
7441
+ });
7442
+ });
7443
+ }
7444
+ function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
7445
+ const entry = (service, account) => keyringEntry(keyring, platform, service, account);
7446
+ const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
7447
+ return {
7448
+ async getPassword(service, account) {
7449
+ const secret = await entry(service, account).getSecret();
7450
+ if (secret !== void 0) return Buffer.from(secret).toString("utf8");
7451
+ const legacy = platform === "linux" ? await readLegacy(service, account) : null;
7452
+ if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
7453
+ return legacy;
7454
+ },
7455
+ setPassword: store,
7456
+ deletePassword: (service, account) => entry(service, account).deleteCredential()
7457
+ };
7458
+ }
7459
+ var keytarCache = void 0;
7460
+ async function loadKeytar() {
7461
+ if (keytarCache !== void 0) return keytarCache;
7462
+ try {
7463
+ const mod = await import("@napi-rs/keyring");
7464
+ const keyring = mod.default ?? mod;
7465
+ keyringEntry(keyring, process.platform, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
7466
+ keytarCache = keytarOverKeyring(keyring);
7467
+ } catch {
7468
+ keytarCache = null;
7469
+ }
7470
+ return keytarCache;
7471
+ }
7472
+ async function keychainGet(account) {
7473
+ const keytar = await loadKeytar();
7474
+ if (keytar === null) return null;
7475
+ try {
7476
+ const value = await keytar.getPassword(KEYCHAIN_SERVICE, account);
7477
+ return value !== null && value.length > 0 ? value : null;
7478
+ } catch {
7479
+ return null;
7480
+ }
7481
+ }
7482
+ async function keychainDelete(account) {
7483
+ const keytar = await loadKeytar();
7484
+ if (keytar === null) return false;
7485
+ try {
7486
+ return await keytar.deletePassword(KEYCHAIN_SERVICE, account);
7487
+ } catch {
7488
+ return false;
7489
+ }
7490
+ }
7491
+ function parseSeed(text, where) {
7492
+ const seed = Buffer.from(text, "base64url");
7493
+ if (!/^[A-Za-z0-9_-]{43}$/.test(text) || seed.toString("base64url") !== text) throw new SeedInvalidError(where);
7494
+ return new Uint8Array(seed);
7495
+ }
7496
+ async function readSeed(stateDir, env = process.env) {
7497
+ const fromEnv = env[SEED_ENV_VAR]?.trim();
7498
+ if (fromEnv) return { seed: parseSeed(fromEnv, SEED_ENV_VAR), source: "env" };
7499
+ const stored = await keychainGet(KEYCHAIN_ACCOUNT);
7500
+ if (stored !== null) return { seed: parseSeed(stored, `keychain entry ${KEYCHAIN_SERVICE}/${KEYCHAIN_ACCOUNT}`), source: "keychain" };
7501
+ const file = (0, import_node_path4.join)(stateDir, SEED_FILE_NAME);
7502
+ let text;
7503
+ try {
7504
+ text = await import_node_fs3.promises.readFile(file, "utf8");
7505
+ } catch (err2) {
7506
+ if (err2.code === "ENOENT") return null;
7507
+ throw err2;
7508
+ }
7509
+ return { seed: parseSeed(text.trim(), file), source: "file" };
7510
+ }
7511
+ async function storeSeed(stateDir, seed) {
7512
+ const text = Buffer.from(seed).toString("base64url");
7513
+ const keytar = await loadKeytar();
7514
+ if (keytar !== null) {
7515
+ try {
7516
+ await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, text);
7517
+ return "keychain";
7518
+ } catch {
7519
+ }
7520
+ }
7521
+ const fh = await import_node_fs3.promises.open((0, import_node_path4.join)(stateDir, SEED_FILE_NAME), "wx", 384);
7522
+ try {
7523
+ await fh.writeFile(`${text}
7524
+ `, "utf8");
7525
+ await fh.sync();
7526
+ } finally {
7527
+ await fh.close();
7528
+ }
7529
+ return "file";
7530
+ }
7531
+ function statePassphrase(seed) {
7532
+ return Buffer.from((0, import_node_crypto3.hkdfSync)("sha256", seed, "seal-mcp:state:v1", "passphrase", 32)).toString("base64url");
7533
+ }
7534
+ async function legacyPassphrase(env) {
7535
+ const stored = await keychainGet(LEGACY_KEYCHAIN_ACCOUNT);
7536
+ if (stored !== null) return { passphrase: stored, source: "keychain" };
7537
+ const fromEnv = env[PASSPHRASE_ENV_VAR];
7538
+ return fromEnv ? { passphrase: fromEnv, source: "env" } : null;
7539
+ }
7540
+ async function tryOpen(passphrase, statePath) {
7541
+ try {
7542
+ return await loadState(passphrase, statePath);
7543
+ } catch (err2) {
7544
+ if (err2 instanceof StatePassphraseInvalid) return null;
7545
+ throw err2;
7546
+ }
7547
+ }
7548
+ async function requireIdentity(view, seed, statePath) {
7549
+ const { secret } = await loadIdentity(view.identity);
7550
+ if (!Buffer.from(secret).equals(Buffer.from(seed))) throw new IdentityConflictError(statePath);
7551
+ }
7552
+ function openAgentState(path, env = process.env) {
7553
+ return withStateLock(path.dir, async () => {
7554
+ let root = await readSeed(path.dir, env);
7555
+ if (!await import_node_fs3.promises.stat(path.statePath).then(() => true, () => false)) {
7556
+ if (root === null) {
7557
+ const { secret } = await generateIdentity();
7558
+ root = { seed: secret, source: await storeSeed(path.dir, secret) };
7559
+ }
7560
+ const passphrase2 = statePassphrase(root.seed);
7561
+ const identity = await identityFromSeed(root.seed);
7562
+ await saveState(createEmptyState(identityToFields(identity)), passphrase2, path.statePath);
7563
+ return { root, passphrase: passphrase2, created: true, moved: null };
7564
+ }
7565
+ if (root !== null) {
7566
+ const view2 = await tryOpen(statePassphrase(root.seed), path.statePath);
7567
+ if (view2 !== null) {
7568
+ await requireIdentity(view2, root.seed, path.statePath);
7569
+ return { root, passphrase: statePassphrase(root.seed), created: false, moved: null };
7570
+ }
7571
+ }
7572
+ const legacy = await legacyPassphrase(env);
7573
+ const view = legacy === null ? null : await tryOpen(legacy.passphrase, path.statePath);
7574
+ if (legacy === null || view === null) throw new IdentityConflictError(path.statePath);
7575
+ if (root !== null) await requireIdentity(view, root.seed, path.statePath);
7576
+ else {
7577
+ const { secret } = await loadIdentity(view.identity);
7578
+ root = { seed: secret, source: await storeSeed(path.dir, secret) };
7579
+ }
7580
+ const passphrase = statePassphrase(root.seed);
7581
+ await saveState(view, passphrase, path.statePath);
7582
+ await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
7583
+ return { root, passphrase, created: false, moved: legacy.source };
7584
+ });
7585
+ }
7586
+ async function deleteRootSecret(stateDir) {
7587
+ let seedFile = true;
7588
+ await import_node_fs3.promises.unlink((0, import_node_path4.join)(stateDir, SEED_FILE_NAME)).catch((err2) => {
7589
+ if (err2.code !== "ENOENT") throw err2;
7590
+ seedFile = false;
7591
+ });
7592
+ const seed = await keychainDelete(KEYCHAIN_ACCOUNT);
7593
+ const legacy = await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
7594
+ return { seedFile, keychain: seed || legacy };
7595
+ }
7596
+ async function probeKeychain() {
7597
+ const keytar = await loadKeytar();
7598
+ if (keytar === null) return { available: false, hasSeed: false };
7599
+ try {
7600
+ const value = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
7601
+ return { available: true, hasSeed: value !== null && value.length > 0 };
7602
+ } catch {
7603
+ return { available: false, hasSeed: false };
7604
+ }
7605
+ }
7606
+
7607
+ // src/server.ts
7608
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
7609
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7610
+
7611
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
7612
+ var external_exports = {};
7613
+ __export(external_exports, {
7614
+ BRAND: () => BRAND,
7615
+ DIRTY: () => DIRTY,
7616
+ EMPTY_PATH: () => EMPTY_PATH,
7617
+ INVALID: () => INVALID,
7618
+ NEVER: () => NEVER,
7619
+ OK: () => OK,
7620
+ ParseStatus: () => ParseStatus,
7621
+ Schema: () => ZodType,
7622
+ ZodAny: () => ZodAny,
7623
+ ZodArray: () => ZodArray,
7624
+ ZodBigInt: () => ZodBigInt,
7625
+ ZodBoolean: () => ZodBoolean,
7626
+ ZodBranded: () => ZodBranded,
7627
+ ZodCatch: () => ZodCatch,
7628
+ ZodDate: () => ZodDate,
7629
+ ZodDefault: () => ZodDefault,
7630
+ ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
7631
+ ZodEffects: () => ZodEffects,
7632
+ ZodEnum: () => ZodEnum,
7633
+ ZodError: () => ZodError,
7634
+ ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
7635
+ ZodFunction: () => ZodFunction,
7636
+ ZodIntersection: () => ZodIntersection,
7637
+ ZodIssueCode: () => ZodIssueCode,
7638
+ ZodLazy: () => ZodLazy,
7639
+ ZodLiteral: () => ZodLiteral,
7640
+ ZodMap: () => ZodMap,
7641
+ ZodNaN: () => ZodNaN,
7642
+ ZodNativeEnum: () => ZodNativeEnum,
7643
+ ZodNever: () => ZodNever,
7644
+ ZodNull: () => ZodNull,
7645
+ ZodNullable: () => ZodNullable,
7646
+ ZodNumber: () => ZodNumber,
7647
+ ZodObject: () => ZodObject,
7648
+ ZodOptional: () => ZodOptional,
7649
+ ZodParsedType: () => ZodParsedType,
7650
+ ZodPipeline: () => ZodPipeline,
7651
+ ZodPromise: () => ZodPromise,
7652
+ ZodReadonly: () => ZodReadonly,
7653
+ ZodRecord: () => ZodRecord,
7654
+ ZodSchema: () => ZodType,
7655
+ ZodSet: () => ZodSet,
7656
+ ZodString: () => ZodString,
7657
+ ZodSymbol: () => ZodSymbol,
7658
+ ZodTransformer: () => ZodEffects,
7659
+ ZodTuple: () => ZodTuple,
7660
+ ZodType: () => ZodType,
7661
+ ZodUndefined: () => ZodUndefined,
7662
+ ZodUnion: () => ZodUnion,
7663
+ ZodUnknown: () => ZodUnknown,
7664
+ ZodVoid: () => ZodVoid,
7665
+ addIssueToContext: () => addIssueToContext,
7666
+ any: () => anyType,
7667
+ array: () => arrayType,
7668
+ bigint: () => bigIntType,
7669
+ boolean: () => booleanType,
7670
+ coerce: () => coerce,
7671
+ custom: () => custom,
7672
+ date: () => dateType,
7673
+ datetimeRegex: () => datetimeRegex,
7674
+ defaultErrorMap: () => en_default,
7675
+ discriminatedUnion: () => discriminatedUnionType,
7676
+ effect: () => effectsType,
7677
+ enum: () => enumType,
7678
+ function: () => functionType,
7679
+ getErrorMap: () => getErrorMap,
7680
+ getParsedType: () => getParsedType,
7681
+ instanceof: () => instanceOfType,
7682
+ intersection: () => intersectionType,
7683
+ isAborted: () => isAborted,
7684
+ isAsync: () => isAsync,
7685
+ isDirty: () => isDirty,
7686
+ isValid: () => isValid,
7687
+ late: () => late,
7688
+ lazy: () => lazyType,
7689
+ literal: () => literalType,
7690
+ makeIssue: () => makeIssue,
7691
+ map: () => mapType,
7692
+ nan: () => nanType,
7693
+ nativeEnum: () => nativeEnumType,
7694
+ never: () => neverType,
7695
+ null: () => nullType,
7696
+ nullable: () => nullableType,
7697
+ number: () => numberType,
7698
+ object: () => objectType,
7699
+ objectUtil: () => objectUtil,
7700
+ oboolean: () => oboolean,
7701
+ onumber: () => onumber,
7702
+ optional: () => optionalType,
7703
+ ostring: () => ostring,
7704
+ pipeline: () => pipelineType,
7705
+ preprocess: () => preprocessType,
7706
+ promise: () => promiseType,
7707
+ quotelessJson: () => quotelessJson,
7708
+ record: () => recordType,
7709
+ set: () => setType,
7710
+ setErrorMap: () => setErrorMap,
7711
+ strictObject: () => strictObjectType,
7712
+ string: () => stringType,
7713
+ symbol: () => symbolType,
7714
+ transformer: () => effectsType,
7715
+ tuple: () => tupleType,
7716
+ undefined: () => undefinedType,
7717
+ union: () => unionType,
7718
+ unknown: () => unknownType,
7719
+ util: () => util,
7720
+ void: () => voidType
7721
+ });
7722
+
7723
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
7724
+ var util;
7725
+ (function(util2) {
7726
+ util2.assertEqual = (_) => {
7727
+ };
7728
+ function assertIs(_arg) {
7729
+ }
7730
+ util2.assertIs = assertIs;
7731
+ function assertNever(_x) {
7732
+ throw new Error();
7733
+ }
7734
+ util2.assertNever = assertNever;
7735
+ util2.arrayToEnum = (items) => {
7736
+ const obj = {};
7737
+ for (const item of items) {
7738
+ obj[item] = item;
7739
+ }
7740
+ return obj;
7741
+ };
7742
+ util2.getValidEnumValues = (obj) => {
7743
+ const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
7744
+ const filtered = {};
7745
+ for (const k of validKeys) {
7746
+ filtered[k] = obj[k];
7747
+ }
7455
7748
  return util2.objectValues(filtered);
7456
7749
  };
7457
7750
  util2.objectValues = (obj) => {
@@ -11371,13 +11664,13 @@ var seal_mcp_v1_default = {
11371
11664
  progress: "When a `tools/call` carries `_meta.progressToken`, `seal_share` and `seal_open mode=file` send `notifications/progress` with `progress` = ciphertext bytes done (upload: confirmed by storage) and `total` = ciphertext bytes in all; at most one per second, `progress` strictly increasing, the last one at `progress = total`."
11372
11665
  },
11373
11666
  server_instructions: {
11374
- text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files).",
11667
+ text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files). Never read or print a SEAL handoff file.",
11375
11668
  note: "The `instructions` of the `initialize` result (SPEC-AGENTS \xA78.1). Hosts that defer MCP tools behind a tool search (Claude Code) show the model only tool names until it searches, but put these instructions into the system prompt: this is the one text a model reads before it decides to look for a tool, or reads the file it was asked to send."
11376
11669
  },
11377
11670
  tools: {
11378
11671
  seal_share: {
11379
11672
  title: "Share a file via SEAL",
11380
- description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for any size (folders, dumps, exports, builds, videos \u2014 up to 1 GB free, more after one card payment by the user). Default mode `handoff`: the link goes to the user's clipboard or a 0600 file and you get only a handle. Mode `forward`: a link only the holder of the given X25519 key can open \u2014 use it for another agent. Never paste secrets or huge content into the chat instead.",
11673
+ description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for large files (folders, dumps, exports, builds, videos \u2014 up to 1 GB free, more after one card payment by the user). Default mode `handoff`: the link goes to the user's clipboard and you get a handle. Where there is no clipboard it goes to a 0600 file and you get its path: do not read that file and do not print it. Mode `forward`: a link only the holder of the given X25519 key can open \u2014 use it for another agent. Never paste secrets or huge content into the chat instead.",
11381
11674
  annotations: { destructiveHint: false, openWorldHint: true },
11382
11675
  input: {
11383
11676
  properties: {
@@ -11421,13 +11714,15 @@ var seal_mcp_v1_default = {
11421
11714
  required: ["handle", "receipt", "handoff_channel"],
11422
11715
  properties: {
11423
11716
  handle: { type: "string", pattern: "^h_[A-Za-z0-9_-]{12,}$", description: "Opaque server-internal ID. Used by `seal_revoke` and other tools. 12+ base64url chars after the `h_` prefix \u2014 the alphabet includes `-` and `_` (generateHandle emits base64url), so the pattern must too." },
11424
- receipt: { type: "string", description: "Plain-text status the model should relay verbatim to the user. e.g. 'The seal link is on your clipboard. Paste it to the recipient.' In ephemeral mode (see `passphrase_delivery`) it adds that the handle lives until the server restarts and the seal's expiry does not depend on it." },
11717
+ receipt: { type: "string", description: "Plain-text status the model should relay verbatim to the user. e.g. 'The seal link is on your clipboard. Paste it to the recipient.' In ephemeral mode (see `root_secret.ephemeral_mode`) it adds that the handle lives until the server restarts and the seal's expiry does not depend on it." },
11425
11718
  handoff_channel: { type: "string", enum: ["clipboard", "file"] },
11426
11719
  handoff_file: { type: "string", description: "ONLY when handoff_channel='file'. Absolute path to a 0o600 file containing the share link and a newline." },
11427
11720
  expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
11428
11721
  },
11429
11722
  MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
11430
- rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
11723
+ rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode.",
11724
+ receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
11725
+ receipt_file: "The seal link is in {file} (mode 0600, removed automatically later). Give it to the recipient. Anyone who can read files on this machine as this user, this agent included, can read it. In a cloud or container session prefer mode=forward to the recipient\u2019s key."
11431
11726
  },
11432
11727
  output_forward: {
11433
11728
  type: "object",
@@ -11755,7 +12050,7 @@ var seal_mcp_v1_default = {
11755
12050
  type: "object",
11756
12051
  required: ["x25519_secret_b64", "x25519_public_b64"],
11757
12052
  properties: {
11758
- x25519_secret_b64: { type: "string", description: "32 raw bytes, base64-padded. Generated once, at `sealnet-mcp init` or at the first `sealnet-mcp serve` without state. Lost passphrase = lost identity = unable to open incoming targeted seals; handoff continues to work without identity." },
12053
+ x25519_secret_b64: { type: "string", description: "32 raw bytes, base64url. The seed of `root_secret`: must equal it, else the server refuses with `identity_conflict`. Lost seed = lost address = incoming targeted seals unopenable; handoff continues to work without identity." },
11759
12054
  x25519_public_b64: { type: "string", description: "32 raw bytes, derived from secret. Safe to publish; advertised so other parties can target this MCP." }
11760
12055
  }
11761
12056
  },
@@ -11776,7 +12071,7 @@ var seal_mcp_v1_default = {
11776
12071
  },
11777
12072
  pending: {
11778
12073
  type: "object",
11779
- description: "Durable operation journal (ISSUE-0623), keyed by handle. A seal_share journals BEFORE its first remote effect and checkpoints every step; completion removes the entry in the same envelope write that inserts the durable handle. OPTIONAL: envelopes written before the journal existed omit it and readers normalise to {}. Recovery semantics at server start: 'preparing' (no confirmed remote effect) is dropped; 'uploaded'/'awaiting_payment'/'compensating' (URL never delivered) is revoked, AWAITED \u2014 an unpaid seal cannot be finished after a restart, and a payment that lands after the revoke is refunded by the server; 'delivering' (URL possibly already delivered) is ADOPTED as a durable handle \u2014 never revoked, so the owner keeps control of a link the user may already hold.",
12074
+ description: "Durable operation journal (ISSUE-0623), keyed by handle. A seal_share journals BEFORE its first remote effect and checkpoints every step; completion removes the entry in the same envelope write that inserts the durable handle. OPTIONAL: envelopes written before the journal existed omit it and readers normalise to {}. Recovery semantics at server start, for entries whose `owner` cannot still be running (several MCP hosts share one state, SPEC-ADDRESS F1 \xA74): 'preparing' (no confirmed remote effect) is dropped; 'uploaded'/'awaiting_payment'/'compensating' (URL never delivered) is revoked, AWAITED \u2014 an unpaid seal cannot be finished after a restart, and a payment that lands after the revoke is refunded by the server; 'delivering' (URL possibly already delivered) is ADOPTED as a durable handle \u2014 never revoked, so the owner keeps control of a link the user may already hold.",
11780
12075
  additionalProperties: {
11781
12076
  type: "object",
11782
12077
  required: ["tool", "mode", "started_at", "owner_token_b64", "expires_at", "step"],
@@ -11789,6 +12084,12 @@ var seal_mcp_v1_default = {
11789
12084
  step: { type: "string", enum: ["preparing", "awaiting_payment", "uploaded", "delivering", "compensating"] },
11790
12085
  seal_id: { type: "string", pattern: "^[A-Za-z0-9_-]{6,20}$", description: "Set once the upload confirms; (seal_id, owner_token_b64) is the full revoke capability." },
11791
12086
  handoff_channels_attempted: { type: "array", items: { type: "string", enum: ["clipboard", "file"] }, description: "Channels performHandoff will try, in order (handoff mode only; journaled before delivery starts)." },
12087
+ owner: {
12088
+ type: "object",
12089
+ required: ["pid", "hostname"],
12090
+ properties: { pid: { type: "integer" }, hostname: { type: "string" } },
12091
+ description: "The process running the operation. Recovery leaves an entry alone while that process may still run: alive on this host, or on another host, which cannot be probed (the state lock's rule). Entries without it, from 0.2.x, are recovered as crashed."
12092
+ },
11792
12093
  compensation: {
11793
12094
  type: "object",
11794
12095
  required: ["action", "outcome", "error", "at"],
@@ -11818,29 +12119,22 @@ var seal_mcp_v1_default = {
11818
12119
  }
11819
12120
  }
11820
12121
  }
11821
- }
11822
- },
11823
- passphrase_delivery: {
11824
- ordered_priority: [
11825
- {
11826
- rank: 1,
11827
- method: "os_keychain",
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.",
11829
- default: true
11830
- },
11831
- {
11832
- rank: 2,
11833
- method: "env_var",
11834
- implementation: "$SEAL_MCP_PASSPHRASE. Visible to same-user attackers via /proc/<pid>/environ on Linux. Warning logged on stderr at startup. Use only in CI / headless containers."
11835
- },
11836
- {
11837
- rank: 3,
11838
- method: "REFUSED",
11839
- implementation: "Cleartext passphrase file on disk is explicitly refused even if user passes `--passphrase-file \u2026`. Error message points at options 1 and 2."
11840
- }
12122
+ }
12123
+ },
12124
+ root_secret: {
12125
+ seed: "The agent's one root secret (SPEC-ADDRESS F1): a 32-byte X25519 secret, as text 43 base64url characters \u2014 the same bytes as `state.identity.x25519_secret_b64`, the CLI's `identity.v1.json::x25519_secret_b64url`, the SDK's `KeyPair.secretKey` and sealnet's `keygen().secret_key`. Its public half is the agent's address; it must survive a restart, a second MCP host on the machine and a move to the cloud.",
12126
+ ordered_sources: [
12127
+ { rank: 1, source: "env", implementation: "$SEAL_SEED. How a container or a cloud session gets the same address as at home." },
12128
+ { rank: 2, source: "keychain", 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. Service 'seal-mcp', account 'seed', the seed as UTF-8 bytes." },
12129
+ { rank: 3, source: "file", implementation: "`<state_dir>/seed`, mode 0600 in the 0700 state directory, created on the first start where there is no keychain. The CLI's threat model (`secrets.rs`): a process running as this user reads it either way, keychain or file." }
11841
12130
  ],
11842
- ephemeral_mode: "First `serve` without state where neither the keychain nor SEAL_MCP_PASSPHRASE is available (container, CI), or `serve --ephemeral`: identity, handles, journal and audit live only in process memory and nothing is written to disk (rank 3 stays refused). The `seal_share` receipt says the handle lives until the server restarts and the seal's expiry does not depend on it.",
11843
- lost_passphrase_recovery: "Passphrase is NOT recoverable. `sealnet-mcp init --force` mints a new identity, but old targeted seals (addressed to the old pubkey) become permanently unopenable. Handoff seals are not affected (they don't use identity)."
12131
+ created: "The first `serve` (or `init`) with no seed in any source makes one and keeps it in the keychain, or in the file where there is none. `init` asks nothing.",
12132
+ state_passphrase: "base64url(HKDF-SHA256(ikm = seed, salt = 'seal-mcp:state:v1', info = 'passphrase', 32 bytes)). The state envelope (Argon2id \u2192 AES-256-GCM) is unchanged; only where its passphrase comes from changed. One root secret instead of a passphrase and a key.",
12133
+ identity_conflict: "`state.identity` must be the seed. A state that opens neither with the seed's passphrase nor with a migration passphrase belongs to another key (a foreign SEAL_SEED): `serve` refuses with `identity_conflict`, exit 3. Unset SEAL_SEED, or `sealnet-mcp reset --force`. The address never changes silently.",
12134
+ migration_0_2: "Once, when no seed exists in any source and a state does: open it with the old passphrase (keychain entry 'seal-mcp'/'state', including the Linux entry keytar wrote, or $SEAL_MCP_PASSPHRASE), take the seed from `state.identity`, store it (keychain 'seed', else the file), save the same state under the seed's passphrase (atomic, as every save), delete the old keychain entry. $SEAL_MCP_PASSPHRASE is read only here and warns that it is no longer used. Removed in the first minor release after no 0.2.x state is left, the condition `readKeytarLinuxEntry` carries.",
12135
+ ephemeral_mode: "Only `serve --ephemeral` or $SEAL_MCP_EPHEMERAL=1: a new key each start; identity, handles, journal and audit live only in process memory, nothing is written to disk. The `seal_share` receipt says the handle lives until the server restarts and the seal's expiry does not depend on it.",
12136
+ several_hosts: "Every MCP host of one user on one machine (Claude Desktop, Claude Code, Cursor) runs its own `serve` on the same state: one seed, one address. The state lock is taken per write, not for a process lifetime: each change re-reads `state.json`, applies this process's insertions and explicit deletions and saves. Waiting for the lock ends after 5 s with `state_busy`.",
12137
+ lost_seed: "The seed is the address: lost (`reset --force`, a wiped keychain with no file) means links addressed to the old key stay unopenable and paired owners must pair again; handoff seals are unaffected. `sealnet-mcp seed --force` prints it, to move the agent by setting SEAL_SEED elsewhere."
11844
12138
  },
11845
12139
  handoff_delivery: {
11846
12140
  primary_channel: "system_clipboard",
@@ -11916,11 +12210,12 @@ var seal_mcp_v1_default = {
11916
12210
  pro_file_not_granted: "seal_pro_file_get: the room's seal is visible but a file's key envelope is refused to this agent.",
11917
12211
  pro_failed: "seal_pro_*: SEAL Pro answered with another error; the message has the status.",
11918
12212
  state_persist_failed: "seal_share: the journal checkpoint (`uploaded` or `delivering`) could not be made durable. The fresh seal is auto-revoked (compensation outcome embedded in the message) because delivering a capability the journal cannot account for is worse than failing the call.",
12213
+ state_busy: "Another sealnet-mcp process held the state lock for more than 5 s. Nothing was changed by this call; safe to retry.",
11919
12214
  runtime_unsupported: "Node runtime is missing a global the bundle assumes (e.g. `globalThis.File` or `globalThis.Blob`). Upgrade to Node \u226520.",
11920
12215
  internal_inconsistency: "Server detected a state branch the type-system thought was unreachable (e.g. forward gate passed but recipientPub is null). Always a bug \u2014 caller should report.",
11921
- "state.passphrase_invalid": "AES-256-GCM tag check failed. Likely wrong passphrase in keychain / env; could be tampered file. Server refuses to start.",
12216
+ identity_conflict: "Startup: the state does not open with this seed's passphrase nor with a migration passphrase \u2014 it belongs to another key (a foreign SEAL_SEED), or it was tampered with. Server refuses to start (exit 3); see `root_secret.identity_conflict`.",
11922
12217
  "state.corrupt": "Envelope JSON malformed or version unknown. Server refuses to start; user must rerun `sealnet-mcp init --force` (lose old data) or restore from backup.",
11923
- state_dir_unsafe: "Startup: the state directory is a symlink or not a real directory \u2014 the lifetime lock and the envelope it protects would resolve to different places. Server refuses to start.",
12218
+ state_dir_unsafe: "The state directory is a symlink or not a real directory \u2014 the state lock and the envelope it protects would resolve to different places. Refused.",
11924
12219
  base_url_invalid: "Startup/config: backend or public-host base URL is not an absolute http(s) URL, or carries a query string / fragment / embedded credentials. Every derived URL would be corrupt, so the value is rejected at configuration time."
11925
12220
  },
11926
12221
  current_format_version: 1,
@@ -11947,7 +12242,7 @@ function safeFilename(name) {
11947
12242
  return cleaned.length > 0 ? cleaned : "seal-file";
11948
12243
  }
11949
12244
  async function receiveToDisk(opened, dir, name) {
11950
- const fs3 = await import(
12245
+ const fs4 = await import(
11951
12246
  /* webpackIgnore: true */
11952
12247
  /* @vite-ignore */
11953
12248
  "fs/promises"
@@ -11957,10 +12252,10 @@ async function receiveToDisk(opened, dir, name) {
11957
12252
  /* @vite-ignore */
11958
12253
  "path"
11959
12254
  );
11960
- await fs3.mkdir(dir, { recursive: true });
12255
+ await fs4.mkdir(dir, { recursive: true });
11961
12256
  const suffix = Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("");
11962
12257
  const tmp = path.join(dir, `.seal-partial-${suffix}`);
11963
- const fh = await fs3.open(tmp, "wx", 384);
12258
+ const fh = await fs4.open(tmp, "wx", 384);
11964
12259
  let placed = false;
11965
12260
  try {
11966
12261
  await opened.stream.pipeTo(
@@ -11978,12 +12273,12 @@ async function receiveToDisk(opened, dir, name) {
11978
12273
  for (let i = 0; i < 1e3; i++) {
11979
12274
  const candidate = path.join(dir, i === 0 ? name : `${stem} (${i})${ext}`);
11980
12275
  try {
11981
- await fs3.link(tmp, candidate);
12276
+ await fs4.link(tmp, candidate);
11982
12277
  } catch (err2) {
11983
12278
  if (err2.code === "EEXIST") continue;
11984
12279
  throw err2;
11985
12280
  }
11986
- await fs3.unlink(tmp);
12281
+ await fs4.unlink(tmp);
11987
12282
  placed = true;
11988
12283
  return { path: candidate, sizeBytes, sha256: sha2562 };
11989
12284
  }
@@ -11991,7 +12286,7 @@ async function receiveToDisk(opened, dir, name) {
11991
12286
  } finally {
11992
12287
  if (!placed) {
11993
12288
  await fh.close().catch(() => void 0);
11994
- await fs3.unlink(tmp).catch(() => void 0);
12289
+ await fs4.unlink(tmp).catch(() => void 0);
11995
12290
  }
11996
12291
  }
11997
12292
  }
@@ -12351,320 +12646,102 @@ async function loadWorkloadCredentials(env = process.env) {
12351
12646
  const path = workloadCredentialsPath(env);
12352
12647
  let text;
12353
12648
  try {
12354
- text = await (0, import_promises2.readFile)(path, "utf-8");
12355
- } catch (err2) {
12356
- if (err2.code === "ENOENT") return null;
12357
- throw err2;
12358
- }
12359
- const file = JSON.parse(text);
12360
- if (file.version !== 1 || !file.workload_id || !file.base_url || !file.seed_b64) {
12361
- throw new Error(`${path} is not a SEAL Pro workload credentials file`);
12362
- }
12363
- return { workloadId: file.workload_id, baseUrl: file.base_url, seedB64: file.seed_b64 };
12364
- }
12365
-
12366
- // src/recovery.ts
12367
- function nowIso(now) {
12368
- return (now?.() ?? /* @__PURE__ */ new Date()).toISOString();
12369
- }
12370
- async function recoverPendingOperations(view, client, opts = {}) {
12371
- const log4 = opts.log ?? ((line) => void process.stderr.write(`${line}
12372
- `));
12373
- const report = { adopted: [], revoked: [], stillFailing: [], dropped: [] };
12374
- let changed = false;
12375
- for (const [handle, op] of Object.entries(view.pending)) {
12376
- if (op.tool !== "seal_share") {
12377
- log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
12378
- continue;
12379
- }
12380
- if (op.seal_id === void 0) {
12381
- delete view.pending[handle];
12382
- report.dropped.push(handle);
12383
- changed = true;
12384
- log4(`[seal-mcp] recovery: dropped pre-upload journal entry ${handle} (no remote effect)`);
12385
- continue;
12386
- }
12387
- if (op.step === "delivering") {
12388
- adoptAsHandle(view, handle, op, opts.now);
12389
- report.adopted.push(handle);
12390
- changed = true;
12391
- log4(
12392
- `[seal-mcp] recovery: adopted ${handle} as a durable handle \u2014 handoff was in flight (attempted: ${(op.handoff_channels_attempted ?? []).join("\u2192") || "unknown"}); the URL may have been delivered, so the seal stays live under owner control`
12393
- );
12394
- continue;
12395
- }
12396
- if (op.backend_url !== void 0 && op.backend_url !== client.baseUrl) {
12397
- op.step = "compensating";
12398
- op.compensation = {
12399
- action: "revoke",
12400
- outcome: "failed",
12401
- error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
12402
- at: nowIso(opts.now)
12403
- };
12404
- report.stillFailing.push(handle);
12405
- changed = true;
12406
- log4(
12407
- `[seal-mcp] recovery: NOT compensating ${handle} \u2014 it belongs to ${op.backend_url} but this server talks to ${client.baseUrl}; kept in journal until the server runs against the original backend`
12408
- );
12409
- continue;
12410
- }
12411
- try {
12412
- await client.revoke(op.seal_id, op.owner_token_b64);
12413
- delete view.pending[handle];
12414
- report.revoked.push(handle);
12415
- changed = true;
12416
- log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
12417
- } catch (err2) {
12418
- const msg = err2 instanceof Error ? err2.message : String(err2);
12419
- op.step = "compensating";
12420
- op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
12421
- report.stillFailing.push(handle);
12422
- changed = true;
12423
- log4(
12424
- `[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
12425
- );
12426
- }
12427
- }
12428
- return { report, changed };
12429
- }
12430
- function adoptAsHandle(view, handle, op, now) {
12431
- const sealId = op.seal_id;
12432
- if (view.handles[handle] === void 0) {
12433
- view.handles[handle] = {
12434
- seal_id: sealId,
12435
- owner_token_b64: op.owner_token_b64,
12436
- created_at: op.started_at,
12437
- mode: op.mode,
12438
- expires_at: op.expires_at,
12439
- // Propagate provenance so a later seal_revoke on the adopted
12440
- // handle still refuses to target the wrong backend.
12441
- backend_url: op.backend_url
12442
- };
12443
- view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
12444
- }
12445
- delete view.pending[handle];
12446
- }
12447
-
12448
- // src/state.ts
12449
- var import_node_crypto3 = require("crypto");
12450
- var import_node_fs2 = require("fs");
12451
- var import_node_path3 = require("path");
12452
- var import_argon2 = __toESM(require("argon2"), 1);
12453
- var FORMAT_TAG = "SEAL_MCP_STATE v1";
12454
- var STATE_VERSION = 1;
12455
- var ARGON2_MEMORY_KIB = 65536;
12456
- var ARGON2_ITERATIONS = 3;
12457
- var ARGON2_PARALLELISM = 4;
12458
- var ARGON2_SALT_BYTES = 16;
12459
- var KDF_BOUNDS = {
12460
- memory_kib: { min: 8192, max: 1048576 },
12461
- // 8 MiB .. 1 GiB
12462
- iterations: { min: 1, max: 16 },
12463
- parallelism: { min: 1, max: 16 }
12464
- };
12465
- var AES_KEY_BYTES = 32;
12466
- var AES_NONCE_BYTES = 12;
12467
- var StatePassphraseInvalid = class extends Error {
12468
- constructor() {
12469
- super("state.passphrase_invalid: AES-256-GCM tag check failed");
12470
- this.name = "StatePassphraseInvalid";
12471
- }
12472
- };
12473
- var StateCorrupt = class extends Error {
12474
- constructor(reason) {
12475
- super(`state.corrupt: ${reason}`);
12476
- this.name = "StateCorrupt";
12477
- }
12478
- };
12479
- function toB642(bytes) {
12480
- return Buffer.from(bytes).toString("base64");
12481
- }
12482
- function fromB642(s) {
12483
- return new Uint8Array(Buffer.from(s, "base64"));
12484
- }
12485
- var CURRENT_KDF_PARAMS = {
12486
- memory_kib: ARGON2_MEMORY_KIB,
12487
- iterations: ARGON2_ITERATIONS,
12488
- parallelism: ARGON2_PARALLELISM
12489
- };
12490
- function boundedKdfParam(name, value) {
12491
- if (typeof value !== "number" || !Number.isInteger(value)) {
12492
- throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
12493
- }
12494
- const { min, max } = KDF_BOUNDS[name];
12495
- if (value < min || value > max) {
12496
- throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
12497
- }
12498
- return value;
12499
- }
12500
- async function deriveKek(passphrase, saltB64, params) {
12501
- const salt = fromB642(saltB64);
12502
- if (salt.length !== ARGON2_SALT_BYTES) {
12503
- throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
12504
- }
12505
- const kek = await import_argon2.default.hash(passphrase, {
12506
- type: import_argon2.default.argon2id,
12507
- raw: true,
12508
- salt: Buffer.from(salt),
12509
- memoryCost: params.memory_kib,
12510
- timeCost: params.iterations,
12511
- parallelism: params.parallelism,
12512
- hashLength: AES_KEY_BYTES
12513
- });
12514
- return new Uint8Array(kek);
12515
- }
12516
- async function aesGcmEncrypt(kek, nonce, plaintext) {
12517
- const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
12518
- const ct = await import_node_crypto3.webcrypto.subtle.encrypt(
12519
- { name: "AES-GCM", iv: nonce, tagLength: 128 },
12520
- key,
12521
- plaintext
12522
- );
12523
- return new Uint8Array(ct);
12524
- }
12525
- async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
12526
- const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
12527
- try {
12528
- const pt = await import_node_crypto3.webcrypto.subtle.decrypt(
12529
- { name: "AES-GCM", iv: nonce, tagLength: 128 },
12530
- key,
12531
- ciphertextWithTag
12532
- );
12533
- return new Uint8Array(pt);
12534
- } catch {
12535
- throw new StatePassphraseInvalid();
12536
- }
12537
- }
12538
- function createEmptyState(identity) {
12539
- return {
12540
- version: STATE_VERSION,
12541
- identity,
12542
- handles: {},
12543
- audit: [],
12544
- pending: {}
12545
- };
12546
- }
12547
- function validateEnvelope(json) {
12548
- if (typeof json !== "object" || json === null) {
12549
- throw new StateCorrupt("envelope is not an object");
12550
- }
12551
- const e = json;
12552
- if (e.version !== STATE_VERSION) {
12553
- throw new StateCorrupt(`unknown version: ${String(e.version)}`);
12554
- }
12555
- if (e.format !== FORMAT_TAG) {
12556
- throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
12557
- }
12558
- const kdf = e.kdf;
12559
- if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
12560
- throw new StateCorrupt("kdf header missing or wrong algo");
12561
- }
12562
- const cipher = e.cipher;
12563
- if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
12564
- throw new StateCorrupt("cipher header missing or wrong algo");
12565
- }
12566
- }
12567
- function validatePlaintext(json) {
12568
- if (typeof json !== "object" || json === null) {
12569
- throw new StateCorrupt("plaintext is not an object");
12570
- }
12571
- const s = json;
12572
- if (s.version !== STATE_VERSION) {
12573
- throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
12574
- }
12575
- const id = s.identity;
12576
- if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
12577
- throw new StateCorrupt("identity missing or malformed");
12578
- }
12579
- if (typeof s.handles !== "object" || s.handles === null) {
12580
- throw new StateCorrupt("handles is not an object");
12581
- }
12582
- if (!Array.isArray(s.audit)) {
12583
- throw new StateCorrupt("audit is not an array");
12584
- }
12585
- if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
12586
- throw new StateCorrupt("pending is not an object");
12587
- }
12588
- }
12589
- async function loadState(passphrase, statePath) {
12590
- const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
12591
- let envelope;
12592
- try {
12593
- envelope = JSON.parse(raw);
12594
- } catch (err2) {
12595
- throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
12596
- }
12597
- validateEnvelope(envelope);
12598
- const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
12599
- memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
12600
- iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
12601
- parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
12602
- });
12603
- const nonce = fromB642(envelope.cipher.nonce_b64);
12604
- if (nonce.length !== AES_NONCE_BYTES) {
12605
- throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
12606
- }
12607
- const ct = fromB642(envelope.cipher.ciphertext_b64);
12608
- const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
12609
- const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
12610
- let plaintext;
12611
- try {
12612
- plaintext = JSON.parse(ptText);
12649
+ text = await (0, import_promises2.readFile)(path, "utf-8");
12613
12650
  } catch (err2) {
12614
- throw new StateCorrupt(`plaintext is not valid JSON: ${err2.message}`);
12651
+ if (err2.code === "ENOENT") return null;
12652
+ throw err2;
12615
12653
  }
12616
- validatePlaintext(plaintext);
12617
- if (plaintext.pending === void 0) {
12618
- plaintext.pending = {};
12654
+ const file = JSON.parse(text);
12655
+ if (file.version !== 1 || !file.workload_id || !file.base_url || !file.seed_b64) {
12656
+ throw new Error(`${path} is not a SEAL Pro workload credentials file`);
12619
12657
  }
12620
- return plaintext;
12658
+ return { workloadId: file.workload_id, baseUrl: file.base_url, seedB64: file.seed_b64 };
12621
12659
  }
12622
- async function saveState(state, passphrase, statePath) {
12623
- validatePlaintext(state);
12624
- const salt = (0, import_node_crypto3.randomBytes)(ARGON2_SALT_BYTES);
12625
- const nonce = (0, import_node_crypto3.randomBytes)(AES_NONCE_BYTES);
12626
- const kek = await deriveKek(passphrase, toB642(salt), CURRENT_KDF_PARAMS);
12627
- const ptBytes = new TextEncoder().encode(JSON.stringify(state));
12628
- const ct = await aesGcmEncrypt(kek, nonce, ptBytes);
12629
- const envelope = {
12630
- version: STATE_VERSION,
12631
- format: FORMAT_TAG,
12632
- kdf: {
12633
- algo: "argon2id",
12634
- memory_kib: ARGON2_MEMORY_KIB,
12635
- iterations: ARGON2_ITERATIONS,
12636
- parallelism: ARGON2_PARALLELISM,
12637
- salt_b64: toB642(salt)
12638
- },
12639
- cipher: {
12640
- algo: "aes-256-gcm",
12641
- nonce_b64: toB642(nonce),
12642
- ciphertext_b64: toB642(ct),
12643
- tag_included: true
12660
+
12661
+ // src/recovery.ts
12662
+ function nowIso(now) {
12663
+ return (now?.() ?? /* @__PURE__ */ new Date()).toISOString();
12664
+ }
12665
+ async function recoverPendingOperations(view, client, opts = {}) {
12666
+ const log4 = opts.log ?? ((line) => void process.stderr.write(`${line}
12667
+ `));
12668
+ const report = { adopted: [], revoked: [], stillFailing: [], dropped: [] };
12669
+ let changed = false;
12670
+ for (const [handle, op] of Object.entries(view.pending)) {
12671
+ if (op.tool !== "seal_share") {
12672
+ log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
12673
+ continue;
12674
+ }
12675
+ if (op.owner !== void 0 && op.owner.pid !== process.pid && holderMayRun(op.owner)) {
12676
+ log4(`[seal-mcp] recovery: left ${handle} alone \u2014 pid ${op.owner.pid} on ${op.owner.hostname} may still run it`);
12677
+ continue;
12678
+ }
12679
+ if (op.seal_id === void 0) {
12680
+ delete view.pending[handle];
12681
+ report.dropped.push(handle);
12682
+ changed = true;
12683
+ log4(`[seal-mcp] recovery: dropped pre-upload journal entry ${handle} (no remote effect)`);
12684
+ continue;
12685
+ }
12686
+ if (op.step === "delivering") {
12687
+ adoptAsHandle(view, handle, op, opts.now);
12688
+ report.adopted.push(handle);
12689
+ changed = true;
12690
+ log4(
12691
+ `[seal-mcp] recovery: adopted ${handle} as a durable handle \u2014 handoff was in flight (attempted: ${(op.handoff_channels_attempted ?? []).join("\u2192") || "unknown"}); the URL may have been delivered, so the seal stays live under owner control`
12692
+ );
12693
+ continue;
12694
+ }
12695
+ if (op.backend_url !== void 0 && op.backend_url !== client.baseUrl) {
12696
+ op.step = "compensating";
12697
+ op.compensation = {
12698
+ action: "revoke",
12699
+ outcome: "failed",
12700
+ error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
12701
+ at: nowIso(opts.now)
12702
+ };
12703
+ report.stillFailing.push(handle);
12704
+ changed = true;
12705
+ log4(
12706
+ `[seal-mcp] recovery: NOT compensating ${handle} \u2014 it belongs to ${op.backend_url} but this server talks to ${client.baseUrl}; kept in journal until the server runs against the original backend`
12707
+ );
12708
+ continue;
12644
12709
  }
12645
- };
12646
- const dir = (0, import_node_path3.dirname)(statePath);
12647
- await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
12648
- const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
12649
- const fh = await import_node_fs2.promises.open(tmp, "w", 384);
12650
- try {
12651
- await fh.writeFile(JSON.stringify(envelope), "utf8");
12652
- await fh.sync();
12653
- } finally {
12654
- await fh.close();
12655
- }
12656
- await import_node_fs2.promises.rename(tmp, statePath);
12657
- try {
12658
- const dh = await import_node_fs2.promises.open(dir, "r");
12659
12710
  try {
12660
- await dh.sync();
12661
- } finally {
12662
- await dh.close();
12711
+ await client.revoke(op.seal_id, op.owner_token_b64);
12712
+ delete view.pending[handle];
12713
+ report.revoked.push(handle);
12714
+ changed = true;
12715
+ log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
12716
+ } catch (err2) {
12717
+ const msg = err2 instanceof Error ? err2.message : String(err2);
12718
+ op.step = "compensating";
12719
+ op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
12720
+ report.stillFailing.push(handle);
12721
+ changed = true;
12722
+ log4(
12723
+ `[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
12724
+ );
12663
12725
  }
12664
- } catch {
12665
12726
  }
12666
- await import_node_fs2.promises.chmod(dir, 448).catch(() => {
12667
- });
12727
+ return { report, changed };
12728
+ }
12729
+ function adoptAsHandle(view, handle, op, now) {
12730
+ const sealId = op.seal_id;
12731
+ if (view.handles[handle] === void 0) {
12732
+ view.handles[handle] = {
12733
+ seal_id: sealId,
12734
+ owner_token_b64: op.owner_token_b64,
12735
+ created_at: op.started_at,
12736
+ mode: op.mode,
12737
+ expires_at: op.expires_at,
12738
+ // Propagate provenance so a later seal_revoke on the adopted
12739
+ // handle still refuses to target the wrong backend.
12740
+ backend_url: op.backend_url
12741
+ };
12742
+ view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
12743
+ }
12744
+ delete view.pending[handle];
12668
12745
  }
12669
12746
 
12670
12747
  // src/tools/context.ts
@@ -12683,19 +12760,18 @@ var HANDLE_REGEX = /^h_[A-Za-z0-9_-]{12,}$/;
12683
12760
  function isValidHandle(handle) {
12684
12761
  return typeof handle === "string" && HANDLE_REGEX.test(handle);
12685
12762
  }
12763
+ function auditEntry(ctx, tool, handle, mode) {
12764
+ return { ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(), tool, handle, mode };
12765
+ }
12686
12766
  function appendAudit(ctx, tool, handle, mode) {
12687
- ctx.state.audit.push({
12688
- ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
12689
- tool,
12690
- handle,
12691
- mode
12692
- });
12767
+ const entry = auditEntry(ctx, tool, handle, mode);
12768
+ return ctx.update((state) => void state.audit.push(entry));
12693
12769
  }
12694
- function insertHandle(ctx, handle, entry) {
12695
- if (ctx.state.handles[handle]) {
12770
+ function insertHandle(state, handle, entry) {
12771
+ if (state.handles[handle]) {
12696
12772
  throw new Error(`insertHandle: collision on ${handle}`);
12697
12773
  }
12698
- ctx.state.handles[handle] = entry;
12774
+ state.handles[handle] = entry;
12699
12775
  }
12700
12776
  function lookupHandle(ctx, handle) {
12701
12777
  const entry = ctx.state.handles[handle];
@@ -12718,6 +12794,7 @@ var ToolError = class extends Error {
12718
12794
  // src/tools/list.ts
12719
12795
  var SINGLE_FILE_COUNT = 1;
12720
12796
  async function sealList(ctx, input = {}) {
12797
+ await appendAudit(ctx, "seal_list", null, null);
12721
12798
  const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).getTime();
12722
12799
  const entries = [];
12723
12800
  for (const [handle, entry] of Object.entries(ctx.state.handles)) {
@@ -12734,15 +12811,13 @@ async function sealList(ctx, input = {}) {
12734
12811
  });
12735
12812
  }
12736
12813
  entries.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
12737
- appendAudit(ctx, "seal_list", null, null);
12738
- await ctx.persist();
12739
12814
  return { entries };
12740
12815
  }
12741
12816
 
12742
12817
  // src/tools/open.ts
12743
12818
  var import_promises3 = require("fs/promises");
12744
12819
  var import_node_os3 = require("os");
12745
- var import_node_path4 = require("path");
12820
+ var import_node_path5 = require("path");
12746
12821
  var INLINE_ALLOWED_MIMES = ["application/json", "text/csv", "text/plain"];
12747
12822
  var INLINE_INJECTION_RISK_MIMES = /* @__PURE__ */ new Set([
12748
12823
  "text/html",
@@ -12820,13 +12895,11 @@ async function sealOpen(ctx, input) {
12820
12895
  if (mode === "inline") {
12821
12896
  const refusal = classifyInlineRefusal(first.mime);
12822
12897
  if (refusal !== null) {
12823
- appendAudit(ctx, "seal_open", handle, mode);
12824
- await ctx.persist();
12898
+ await appendAudit(ctx, "seal_open", handle, mode);
12825
12899
  return { handle, refusal, mime: first.mime, note: refusalNote(refusal, first.mime) };
12826
12900
  }
12827
12901
  if (first.size > INLINE_CIPHERTEXT_MAX_BYTES) {
12828
- appendAudit(ctx, "seal_open", handle, mode);
12829
- await ctx.persist();
12902
+ await appendAudit(ctx, "seal_open", handle, mode);
12830
12903
  return {
12831
12904
  handle,
12832
12905
  refusal: "inline_blocked.too_large",
@@ -12862,12 +12935,10 @@ async function sealOpen(ctx, input) {
12862
12935
  err2
12863
12936
  );
12864
12937
  }
12865
- appendAudit(ctx, "seal_open", handle, mode);
12866
- await ctx.persist();
12938
+ await appendAudit(ctx, "seal_open", handle, mode);
12867
12939
  return { handle, mime: bareMime(first.mime), sizeBytes: plaintext.byteLength, encoding: "utf-8", content };
12868
12940
  }
12869
- appendAudit(ctx, "seal_open", handle, mode);
12870
- await ctx.persist();
12941
+ await appendAudit(ctx, "seal_open", handle, mode);
12871
12942
  return {
12872
12943
  handle,
12873
12944
  filename: first.name,
@@ -12889,7 +12960,7 @@ async function openToDisk(ctx, seal, handle, input) {
12889
12960
  `the link carries no key for any file in seal ${seal.sealId} (files appended after it was minted); ask the sender for a fresh link`
12890
12961
  );
12891
12962
  }
12892
- const dir = (0, import_node_path4.resolve)(input.dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), "Downloads"));
12963
+ const dir = (0, import_node_path5.resolve)(input.dir ?? (0, import_node_path5.join)((0, import_node_os3.homedir)(), "Downloads"));
12893
12964
  try {
12894
12965
  await (0, import_promises3.mkdir)(dir, { recursive: true });
12895
12966
  } catch (err2) {
@@ -12913,15 +12984,14 @@ async function openToDisk(ctx, seal, handle, input) {
12913
12984
  throw streamError(err2, file.id);
12914
12985
  }
12915
12986
  }
12916
- appendAudit(ctx, "seal_open", handle, "file");
12917
- await ctx.persist();
12987
+ await appendAudit(ctx, "seal_open", handle, "file");
12918
12988
  return { handle, files: out };
12919
12989
  }
12920
12990
 
12921
12991
  // src/tools/pro.ts
12922
12992
  var import_promises4 = require("fs/promises");
12923
12993
  var import_node_os4 = require("os");
12924
- var import_node_path5 = require("path");
12994
+ var import_node_path6 = require("path");
12925
12995
  var SECRET_CODES = {
12926
12996
  not_found: "pro_secret_not_found",
12927
12997
  not_granted: "pro_secret_not_granted",
@@ -12946,9 +13016,9 @@ async function asTool(fn, codes = SECRET_CODES) {
12946
13016
  }
12947
13017
  async function sealProSecretUse(source, input) {
12948
13018
  const got = await asTool(() => source.getSecret(input.secret, input.version));
12949
- const dir = await (0, import_promises4.mkdtemp)((0, import_node_path5.join)((0, import_node_os4.tmpdir)(), "seal-mcp-pro-"));
13019
+ const dir = await (0, import_promises4.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "seal-mcp-pro-"));
12950
13020
  const name = (got.path?.split("/").pop() || got.id).replace(/[^A-Za-z0-9._-]/g, "_");
12951
- const path = (0, import_node_path5.join)(dir, name);
13021
+ const path = (0, import_node_path6.join)(dir, name);
12952
13022
  try {
12953
13023
  await (0, import_promises4.writeFile)(path, got.value, { mode: 384, flag: "wx" });
12954
13024
  return { path, sizeBytes: got.value.length, secretId: got.id, version: got.version };
@@ -12962,7 +13032,7 @@ async function sealProSecretRequest(source, input) {
12962
13032
  return asTool(() => source.waitForAccess(asked));
12963
13033
  }
12964
13034
  async function sealProFileGet(source, input) {
12965
- const dir = (0, import_node_path5.resolve)(input.dir ?? (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads"));
13035
+ const dir = (0, import_node_path6.resolve)(input.dir ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Downloads"));
12966
13036
  const ids = input.file ? [input.file] : (await asTool(() => source.listFiles(input.seal), FILE_CODES)).map((f) => f.id);
12967
13037
  const files = [];
12968
13038
  for (const id of ids) {
@@ -12974,16 +13044,16 @@ async function sealProFileGet(source, input) {
12974
13044
 
12975
13045
  // src/tools/request.ts
12976
13046
  var import_node_child_process2 = require("child_process");
12977
- var import_node_fs3 = require("fs");
13047
+ var import_node_fs4 = require("fs");
12978
13048
  var import_promises5 = require("fs/promises");
12979
13049
  var import_node_os5 = require("os");
12980
- var import_node_path6 = require("path");
13050
+ var import_node_path7 = require("path");
12981
13051
  function findSealCli() {
12982
13052
  const names = process.platform === "win32" ? ["seal.exe", "seal.cmd"] : ["seal"];
12983
- for (const dir of (process.env.PATH ?? "").split(import_node_path6.delimiter)) {
13053
+ for (const dir of (process.env.PATH ?? "").split(import_node_path7.delimiter)) {
12984
13054
  for (const name of names) {
12985
- const candidate = (0, import_node_path6.join)(dir, name);
12986
- if (dir && (0, import_node_fs3.existsSync)(candidate)) return candidate;
13055
+ const candidate = (0, import_node_path7.join)(dir, name);
13056
+ if (dir && (0, import_node_fs4.existsSync)(candidate)) return candidate;
12987
13057
  }
12988
13058
  }
12989
13059
  return null;
@@ -13017,8 +13087,7 @@ async function sealRequest(ctx, input, deps) {
13017
13087
  if (input.whereUrl) cliArgs.push("--where", input.whereUrl);
13018
13088
  const local = await (deps.runCli ?? runSealCli)(cliArgs);
13019
13089
  if (local && "files" in local) {
13020
- appendAudit(ctx, "seal_request", handle, "local");
13021
- await ctx.persist();
13090
+ await appendAudit(ctx, "seal_request", handle, "local");
13022
13091
  return {
13023
13092
  handle,
13024
13093
  source: "local",
@@ -13058,8 +13127,7 @@ async function sealRequest(ctx, input, deps) {
13058
13127
  }
13059
13128
  return receiveThroughLink(ctx, handle, intake.id, deps, input.onProgress);
13060
13129
  }
13061
- appendAudit(ctx, "seal_request", handle, "link");
13062
- await ctx.persist();
13130
+ await appendAudit(ctx, "seal_request", handle, "link");
13063
13131
  return { handle, url, emoji, expiresAt: intake.expires_at };
13064
13132
  }
13065
13133
  async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
@@ -13076,12 +13144,11 @@ async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
13076
13144
  );
13077
13145
  }
13078
13146
  await deps.elicitationDone?.(intakeId).catch(() => void 0);
13079
- const dir = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os5.tmpdir)(), "seal-mcp-request-"));
13147
+ const dir = await (0, import_promises5.mkdtemp)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "seal-mcp-request-"));
13080
13148
  const opened = await sealOpen(ctx, { url: `${deps.publicHost}${sharePath}`, mode: "file", dir, onProgress });
13081
13149
  if (!("files" in opened)) throw new ToolError("internal_inconsistency", "seal_open mode=file returned no files");
13082
13150
  ctx.requests?.delete(handle);
13083
- appendAudit(ctx, "seal_request", handle, "link");
13084
- await ctx.persist();
13151
+ await appendAudit(ctx, "seal_request", handle, "link");
13085
13152
  return {
13086
13153
  handle,
13087
13154
  source: "link",
@@ -13094,6 +13161,7 @@ async function sealRevoke(ctx, input) {
13094
13161
  if (!isValidHandle(input.handle)) {
13095
13162
  throw new ToolError("handle_invalid", `handle does not match contract pattern: ${input.handle}`);
13096
13163
  }
13164
+ await ctx.update(() => false);
13097
13165
  const entry = lookupHandle(ctx, input.handle);
13098
13166
  const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
13099
13167
  if (entry.revoked_at) {
@@ -13128,16 +13196,18 @@ async function sealRevoke(ctx, input) {
13128
13196
  err2
13129
13197
  );
13130
13198
  }
13131
- entry.revoked_at = now;
13132
- appendAudit(ctx, "seal_revoke", input.handle, entry.mode);
13133
- await ctx.persist();
13199
+ await ctx.update((state) => {
13200
+ state.handles[input.handle] = { ...entry, revoked_at: now };
13201
+ state.audit.push(auditEntry(ctx, "seal_revoke", input.handle, entry.mode));
13202
+ });
13134
13203
  return { revoked: true, revokedAt: now };
13135
13204
  }
13136
13205
 
13137
13206
  // src/tools/share.ts
13138
- var import_node_fs5 = require("fs");
13207
+ var import_node_fs6 = require("fs");
13139
13208
  var import_promises9 = require("fs/promises");
13140
- var import_node_path9 = require("path");
13209
+ var import_node_os8 = require("os");
13210
+ var import_node_path10 = require("path");
13141
13211
 
13142
13212
  // ../../contracts/limits.json
13143
13213
  var limits_default2 = {
@@ -13171,7 +13241,7 @@ var limits_default2 = {
13171
13241
  // src/handoff.ts
13172
13242
  var import_promises6 = require("fs/promises");
13173
13243
  var import_node_os6 = require("os");
13174
- var import_node_path7 = require("path");
13244
+ var import_node_path8 = require("path");
13175
13245
  var ONE_HOUR_MS = 36e5;
13176
13246
  var HandoffBothChannelsFailedError = class extends Error {
13177
13247
  code = "handoff_both_channels_failed";
@@ -13187,10 +13257,10 @@ var HandoffBothChannelsFailedError = class extends Error {
13187
13257
  function handoffDir(deps) {
13188
13258
  const xdg = process.env.XDG_RUNTIME_DIR;
13189
13259
  const base = deps.tmpdirOverride ?? (xdg && xdg.length > 0 ? xdg : (0, import_node_os6.tmpdir)());
13190
- return (0, import_node_path7.join)(base, "seal-mcp");
13260
+ return (0, import_node_path8.join)(base, "seal-mcp");
13191
13261
  }
13192
13262
  function handoffPath(deps, handle) {
13193
- return (0, import_node_path7.join)(handoffDir(deps), `handoff-${handle}.txt`);
13263
+ return (0, import_node_path8.join)(handoffDir(deps), `handoff-${handle}.txt`);
13194
13264
  }
13195
13265
  async function writeClipboard(text) {
13196
13266
  const mod = await import("clipboardy");
@@ -13257,10 +13327,10 @@ async function performHandoff(payload, deps = {}) {
13257
13327
  }
13258
13328
 
13259
13329
  // src/zip.ts
13260
- var import_node_fs4 = require("fs");
13330
+ var import_node_fs5 = require("fs");
13261
13331
  var import_promises7 = require("fs/promises");
13262
13332
  var import_node_os7 = require("os");
13263
- var import_node_path8 = require("path");
13333
+ var import_node_path9 = require("path");
13264
13334
  var import_node_stream = require("stream");
13265
13335
  var import_promises8 = require("stream/promises");
13266
13336
  var ZIP32_MAX_BYTES = 4294967295;
@@ -13268,16 +13338,16 @@ async function collectFiles(dir) {
13268
13338
  const found = [];
13269
13339
  const walk = async (current) => {
13270
13340
  for (const dirent of await (0, import_promises7.readdir)(current, { withFileTypes: true })) {
13271
- const abs = (0, import_node_path8.join)(current, dirent.name);
13341
+ const abs = (0, import_node_path9.join)(current, dirent.name);
13272
13342
  if (dirent.isDirectory()) await walk(abs);
13273
- else if (dirent.isFile()) found.push({ rel: (0, import_node_path8.relative)(dir, abs).split(import_node_path8.sep).join("/"), abs });
13343
+ else if (dirent.isFile()) found.push({ rel: (0, import_node_path9.relative)(dir, abs).split(import_node_path9.sep).join("/"), abs });
13274
13344
  }
13275
13345
  };
13276
13346
  await walk(dir);
13277
13347
  found.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
13278
13348
  return Promise.all(
13279
13349
  found.map(async ({ rel, abs }) => {
13280
- const file = new File([await (0, import_node_fs4.openAsBlob)(abs)], (0, import_node_path8.basename)(abs));
13350
+ const file = new File([await (0, import_node_fs5.openAsBlob)(abs)], (0, import_node_path9.basename)(abs));
13281
13351
  return Object.defineProperty(file, "webkitRelativePath", { value: rel });
13282
13352
  })
13283
13353
  );
@@ -13287,19 +13357,19 @@ async function planFolderArchive(dir) {
13287
13357
  if (files.length === 0) {
13288
13358
  throw new Error(`directory contains no regular files: ${dir}`);
13289
13359
  }
13290
- const folderName = (0, import_node_path8.basename)((0, import_node_path8.resolve)(dir)) || "folder";
13360
+ const folderName = (0, import_node_path9.basename)((0, import_node_path9.resolve)(dir)) || "folder";
13291
13361
  const size = estimateZipSize({ files, folderName });
13292
13362
  if (size > ZIP32_MAX_BYTES) {
13293
13363
  throw new Error(`the folder's archive would be ${size} bytes, over the ZIP limit of ${ZIP32_MAX_BYTES}`);
13294
13364
  }
13295
13365
  const write = async () => {
13296
- const tempDir = await (0, import_promises7.mkdtemp)((0, import_node_path8.join)((0, import_node_os7.tmpdir)(), "seal-mcp-zip-"));
13366
+ const tempDir = await (0, import_promises7.mkdtemp)((0, import_node_path9.join)((0, import_node_os7.tmpdir)(), "seal-mcp-zip-"));
13297
13367
  const cleanup = () => (0, import_promises7.rm)(tempDir, { recursive: true, force: true });
13298
- const path = (0, import_node_path8.join)(tempDir, `${folderName}.zip`);
13368
+ const path = (0, import_node_path9.join)(tempDir, `${folderName}.zip`);
13299
13369
  try {
13300
13370
  await (0, import_promises8.pipeline)(
13301
13371
  import_node_stream.Readable.fromWeb(createZipStream2({ files, folderName })),
13302
- (0, import_node_fs4.createWriteStream)(path, { mode: 384 })
13372
+ (0, import_node_fs5.createWriteStream)(path, { mode: 384 })
13303
13373
  );
13304
13374
  const written = (await (0, import_promises7.stat)(path)).size;
13305
13375
  if (written !== size) {
@@ -13363,6 +13433,7 @@ function guessMimeFromName(name) {
13363
13433
  }
13364
13434
  var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
13365
13435
  var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
13436
+ var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
13366
13437
  var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
13367
13438
  var PAYMENT_RECEIPT = "The file is larger than 1 GB. Show the user the payment link; the upload continues after payment. Then call seal_share with `payment` set to this handle.";
13368
13439
  function defaultExpire(mode, size) {
@@ -13390,31 +13461,46 @@ var FORWARD_MAX_READS_FORCED = 1;
13390
13461
  function nowIso2(ctx) {
13391
13462
  return (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
13392
13463
  }
13393
- async function persistBestEffort(ctx) {
13464
+ function journal(ctx, handle, op) {
13465
+ return ctx.update((state) => {
13466
+ state.pending[handle] = op;
13467
+ });
13468
+ }
13469
+ function dropJournal(ctx, handle) {
13470
+ return ctx.update((state) => {
13471
+ delete state.pending[handle];
13472
+ });
13473
+ }
13474
+ function complete(ctx, handle, entry) {
13475
+ return ctx.update((state) => {
13476
+ insertHandle(state, handle, entry);
13477
+ state.audit.push(auditEntry(ctx, "seal_share", handle, entry.mode));
13478
+ delete state.pending[handle];
13479
+ });
13480
+ }
13481
+ async function bestEffort(write) {
13394
13482
  try {
13395
- await ctx.persist();
13483
+ await write;
13396
13484
  } catch (err2) {
13397
13485
  const msg = err2 instanceof Error ? err2.message : String(err2);
13398
- process.stderr.write(`[seal-mcp] warn: journal persist failed during compensation: ${msg}
13486
+ process.stderr.write(`[seal-mcp] warn: journal write failed: ${msg}
13399
13487
  `);
13400
13488
  }
13401
13489
  }
13402
- async function compensateRevoke(ctx, handle) {
13403
- const op = ctx.state.pending[handle];
13404
- const sealId = op?.seal_id;
13405
- if (!op || sealId === void 0) {
13490
+ async function compensateRevoke(ctx, handle, op) {
13491
+ const sealId = op.seal_id;
13492
+ if (sealId === void 0) {
13406
13493
  return "no live seal to revoke";
13407
13494
  }
13408
13495
  try {
13409
13496
  await ctx.client.revoke(sealId, op.owner_token_b64);
13410
- delete ctx.state.pending[handle];
13411
- await persistBestEffort(ctx);
13497
+ await bestEffort(dropJournal(ctx, handle));
13412
13498
  return "seal auto-revoked";
13413
13499
  } catch (err2) {
13414
13500
  const msg = err2 instanceof Error ? err2.message : String(err2);
13415
13501
  op.step = "compensating";
13416
13502
  op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso2(ctx) };
13417
- await persistBestEffort(ctx);
13503
+ await bestEffort(journal(ctx, handle, op));
13418
13504
  return `auto-revoke FAILED (${msg}) \u2014 the orphaned seal stays live until its TTL (${op.expires_at}); revoke is journaled and will be retried at next server start`;
13419
13505
  }
13420
13506
  }
@@ -13439,7 +13525,7 @@ async function openSource(path, mime, requestedTier) {
13439
13525
  } catch (err2) {
13440
13526
  throw failed("unable to open", err2);
13441
13527
  }
13442
- let name = (0, import_node_path9.basename)(path);
13528
+ let name = (0, import_node_path10.basename)(path);
13443
13529
  let source = path;
13444
13530
  let cleanup = async () => void 0;
13445
13531
  let tier;
@@ -13465,7 +13551,7 @@ async function openSource(path, mime, requestedTier) {
13465
13551
  }
13466
13552
  let fileBlob;
13467
13553
  try {
13468
- fileBlob = await (0, import_node_fs5.openAsBlob)(source);
13554
+ fileBlob = await (0, import_node_fs6.openAsBlob)(source);
13469
13555
  } catch (err2) {
13470
13556
  await cleanup();
13471
13557
  throw failed("unable to open", err2);
@@ -13586,13 +13672,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
13586
13672
  // backend this seal is actually created on — recovery refuses to
13587
13673
  // "revoke" against a different backend whose 404 would be misread
13588
13674
  // as success.
13589
- backend_url: ctx.client.baseUrl
13675
+ backend_url: ctx.client.baseUrl,
13676
+ // SPEC-ADDRESS F1 §4: another MCP host starting now must not recover
13677
+ // (revoke) this upload while it runs.
13678
+ owner: { pid: process.pid, hostname: (0, import_node_os8.hostname)() }
13590
13679
  };
13591
- ctx.state.pending[handle] = op;
13592
13680
  try {
13593
- await ctx.persist();
13681
+ await journal(ctx, handle, op);
13594
13682
  } catch (err2) {
13595
- delete ctx.state.pending[handle];
13596
13683
  throw new ToolError(
13597
13684
  "state_persist_failed",
13598
13685
  `refusing to upload: could not journal the operation before its first remote effect: ${err2 instanceof Error ? err2.message : String(err2)}`,
@@ -13613,7 +13700,7 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
13613
13700
  // `uploaded` checkpoint after this call is the durability gate.
13614
13701
  onSealCreated: async (sealId) => {
13615
13702
  op.seal_id = sealId;
13616
- await persistBestEffort(ctx);
13703
+ await bestEffort(journal(ctx, handle, op));
13617
13704
  }
13618
13705
  });
13619
13706
  } catch (err2) {
@@ -13622,15 +13709,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
13622
13709
  return awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, expiresAt, session });
13623
13710
  }
13624
13711
  if (op.seal_id !== void 0) {
13625
- const outcome = await compensateRevoke(ctx, handle);
13712
+ const outcome = await compensateRevoke(ctx, handle, op);
13626
13713
  throw new ToolError(
13627
13714
  "upload_failed",
13628
13715
  `seal upload failed after the seal was created: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
13629
13716
  err2
13630
13717
  );
13631
13718
  }
13632
- delete ctx.state.pending[handle];
13633
- await persistBestEffort(ctx);
13719
+ await bestEffort(dropJournal(ctx, handle));
13634
13720
  throw new ToolError(
13635
13721
  "upload_failed",
13636
13722
  `seal upload failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
@@ -13643,9 +13729,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13643
13729
  op.seal_id = uploaded.sealId;
13644
13730
  op.step = "uploaded";
13645
13731
  try {
13646
- await ctx.persist();
13732
+ await journal(ctx, handle, op);
13647
13733
  } catch (err2) {
13648
- const outcome = await compensateRevoke(ctx, handle);
13734
+ const outcome = await compensateRevoke(ctx, handle, op);
13649
13735
  throw new ToolError(
13650
13736
  "state_persist_failed",
13651
13737
  `could not journal the live seal after upload: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
@@ -13658,7 +13744,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13658
13744
  try {
13659
13745
  shareBlobBytes = await encryptShareBlob(fileKeysB64, shareLinkKey, void 0);
13660
13746
  } catch (err2) {
13661
- const outcome = await compensateRevoke(ctx, handle);
13747
+ const outcome = await compensateRevoke(ctx, handle, op);
13662
13748
  throw new ToolError(
13663
13749
  "share_blob_failed",
13664
13750
  `share blob encryption failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
@@ -13670,7 +13756,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13670
13756
  try {
13671
13757
  shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
13672
13758
  } catch (err2) {
13673
- const outcome = await compensateRevoke(ctx, handle);
13759
+ const outcome = await compensateRevoke(ctx, handle, op);
13674
13760
  throw new ToolError(
13675
13761
  "share_link_failed",
13676
13762
  `POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
@@ -13688,7 +13774,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13688
13774
  try {
13689
13775
  fragment = await buildTargetedFragment(shareLinkKey, recipientPub, uploaded.sealId);
13690
13776
  } catch (err2) {
13691
- const outcome = await compensateRevoke(ctx, handle);
13777
+ const outcome = await compensateRevoke(ctx, handle, op);
13692
13778
  const message = err2 instanceof Error ? err2.message : String(err2);
13693
13779
  if (message.includes("non-contributory")) {
13694
13780
  throw new ToolError(
@@ -13709,7 +13795,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13709
13795
  const host = resolvePublicHost(deps.publicHost);
13710
13796
  const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
13711
13797
  if (mode === "forward") {
13712
- insertHandle(ctx, handle, {
13798
+ await complete(ctx, handle, {
13713
13799
  seal_id: uploaded.sealId,
13714
13800
  owner_token_b64: ownerTokenB64,
13715
13801
  created_at: nowIso2(ctx),
@@ -13717,9 +13803,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13717
13803
  expires_at: expiresAt,
13718
13804
  backend_url: ctx.client.baseUrl
13719
13805
  });
13720
- appendAudit(ctx, "seal_share", handle, mode);
13721
- delete ctx.state.pending[handle];
13722
- await ctx.persist();
13723
13806
  return {
13724
13807
  handle,
13725
13808
  shareUrl,
@@ -13731,9 +13814,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13731
13814
  op.step = "delivering";
13732
13815
  op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
13733
13816
  try {
13734
- await ctx.persist();
13817
+ await journal(ctx, handle, op);
13735
13818
  } catch (err2) {
13736
- const outcome = await compensateRevoke(ctx, handle);
13819
+ const outcome = await compensateRevoke(ctx, handle, op);
13737
13820
  throw new ToolError(
13738
13821
  "state_persist_failed",
13739
13822
  `could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
@@ -13744,14 +13827,14 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13744
13827
  try {
13745
13828
  handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
13746
13829
  } catch (err2) {
13747
- const outcome = await compensateRevoke(ctx, handle);
13830
+ const outcome = await compensateRevoke(ctx, handle, op);
13748
13831
  throw new ToolError(
13749
13832
  "handoff_both_channels_failed",
13750
13833
  `both clipboard and file handoff channels failed \u2014 ${outcome}`,
13751
13834
  err2
13752
13835
  );
13753
13836
  }
13754
- insertHandle(ctx, handle, {
13837
+ await complete(ctx, handle, {
13755
13838
  seal_id: uploaded.sealId,
13756
13839
  owner_token_b64: ownerTokenB64,
13757
13840
  created_at: nowIso2(ctx),
@@ -13760,10 +13843,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
13760
13843
  handoff_channel: handoff.channel,
13761
13844
  backend_url: ctx.client.baseUrl
13762
13845
  });
13763
- appendAudit(ctx, "seal_share", handle, mode);
13764
- delete ctx.state.pending[handle];
13765
- await ctx.persist();
13766
- const delivered = handoff.channel === "clipboard" ? "The seal link is on your clipboard. Paste it to the recipient." : `The seal link is in ${handoff.file} (mode 0600, removed automatically later). Give it to the recipient.`;
13846
+ const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
13767
13847
  const verified = uploaded.verification === "verified" ? delivered : `${delivered} The server has not finished verifying the file: for the next few seconds the recipient sees "not ready", after which the link works.`;
13768
13848
  const receipt = ctx.ephemeral ? `${verified} This server keeps no state on disk: the handle lives until it restarts; the seal's expiry does not depend on it.` : verified;
13769
13849
  return {
@@ -13781,12 +13861,12 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
13781
13861
  const tier = prepared.tier;
13782
13862
  const sealId = op.seal_id;
13783
13863
  op.step = "awaiting_payment";
13784
- await persistBestEffort(ctx);
13864
+ await bestEffort(journal(ctx, handle, op));
13785
13865
  let paymentUrl;
13786
13866
  try {
13787
13867
  paymentUrl = await ctx.client.createCheckout(sealId, ownerTokenB64, tier);
13788
13868
  } catch (err2) {
13789
- const outcome = await compensateRevoke(ctx, handle);
13869
+ const outcome = await compensateRevoke(ctx, handle, op);
13790
13870
  throw new ToolError("payment_failed", `POST /billing/checkout failed: ${messageOf3(err2)} (${outcome})`, err2);
13791
13871
  }
13792
13872
  const deadline = paymentDeadline(op.expires_at);
@@ -13796,7 +13876,7 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
13796
13876
  clearTimeout(timer);
13797
13877
  ctx.payments?.delete(handle);
13798
13878
  await prepared.cleanup();
13799
- throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle)})`);
13879
+ throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle, op)})`);
13800
13880
  };
13801
13881
  const finish = async (onProgress) => {
13802
13882
  if (running) throw new ToolError("payment_invalid", `share ${handle} is already being finished`);
@@ -13965,7 +14045,7 @@ function proFileGetOutputToWire(out) {
13965
14045
  }
13966
14046
 
13967
14047
  // src/server.ts
13968
- var SERVER_VERSION = "0.2.4";
14048
+ var SERVER_VERSION = "0.3.0";
13969
14049
  var toolMeta = (name) => {
13970
14050
  const { title, description, annotations } = seal_mcp_v1_default.tools[name];
13971
14051
  return { title, description, annotations };
@@ -13992,41 +14072,29 @@ function progressReporter(extra) {
13992
14072
  }
13993
14073
  async function createServer(options) {
13994
14074
  logEngineToStderr();
13995
- let view;
13996
- let identity;
13997
- let lock = null;
13998
14075
  const client = new SealApi({ baseUrl: resolveBackendUrl(options.backendBaseUrl) });
13999
14076
  const disk = options.state;
14077
+ let view;
14078
+ const update = async (mutate) => {
14079
+ if (disk === null) await mutate(view);
14080
+ else view = await updateState(disk.passphrase, disk.path, mutate);
14081
+ };
14082
+ let identity;
14000
14083
  if (disk === null) {
14001
14084
  identity = await generateIdentity();
14002
14085
  view = createEmptyState(identityToFields(identity));
14003
14086
  } else {
14004
- const held = await acquireStateLock((0, import_node_path10.dirname)(disk.path));
14005
- lock = held;
14006
- try {
14007
- view = await loadState(disk.passphrase, disk.path);
14008
- identity = await loadIdentity(view.identity);
14009
- const { changed } = await recoverPendingOperations(view, client);
14010
- if (changed) {
14011
- await saveState(view, disk.passphrase, disk.path);
14012
- }
14013
- } catch (err2) {
14014
- await held.release();
14015
- throw err2;
14016
- }
14087
+ view = await updateState(disk.passphrase, disk.path, async (state) => (await recoverPendingOperations(state, client)).changed);
14088
+ identity = await loadIdentity(view.identity);
14017
14089
  }
14018
14090
  const publicHost = resolvePublicHost(options.publicHost);
14019
- let persistChain = Promise.resolve();
14020
- const persist = () => {
14021
- if (disk === null) return Promise.resolve();
14022
- persistChain = persistChain.catch(() => void 0).then(() => saveState(view, disk.passphrase, disk.path));
14023
- return persistChain;
14024
- };
14025
14091
  const ctx = {
14026
- state: view,
14092
+ get state() {
14093
+ return view;
14094
+ },
14027
14095
  identity,
14028
14096
  client,
14029
- persist,
14097
+ update,
14030
14098
  ephemeral: disk === null,
14031
14099
  requests: /* @__PURE__ */ new Map(),
14032
14100
  payments: /* @__PURE__ */ new Map()
@@ -14229,18 +14297,12 @@ async function createServer(options) {
14229
14297
  );
14230
14298
  }
14231
14299
  const transport = new import_stdio.StdioServerTransport();
14232
- try {
14233
- await mcp.connect(transport);
14234
- } catch (err2) {
14235
- await lock?.release();
14236
- throw err2;
14237
- }
14300
+ await mcp.connect(transport);
14238
14301
  const close = async () => {
14239
14302
  try {
14240
14303
  await mcp.close();
14241
14304
  } finally {
14242
14305
  await transport.close().catch(() => void 0);
14243
- await lock?.release();
14244
14306
  }
14245
14307
  };
14246
14308
  return { mcp, transport, close };
@@ -14254,7 +14316,7 @@ async function wrapTool(fn, toWire) {
14254
14316
  structuredContent: wire
14255
14317
  };
14256
14318
  } catch (err2) {
14257
- if (err2 instanceof ToolError) {
14319
+ if (err2 instanceof ToolError || err2 instanceof StateBusyError) {
14258
14320
  return {
14259
14321
  isError: true,
14260
14322
  content: [
@@ -14276,7 +14338,6 @@ async function wrapTool(fn, toWire) {
14276
14338
  // src/cli.ts
14277
14339
  var PROGRAM_NAME = "sealnet-mcp";
14278
14340
  var DESCRIPTION = "MCP server for SEAL \u2014 share files via Cursor / Claude Desktop without handing the model the share link. Handoff mode (default) delivers the link out-of-band (clipboard / 0o600 file); the model only gets an opaque handle.";
14279
- var MIN_PASSPHRASE_LEN = 8;
14280
14341
  function logStderr(line) {
14281
14342
  process.stderr.write(`${line}
14282
14343
  `);
@@ -14285,118 +14346,33 @@ function logStdout(line) {
14285
14346
  process.stdout.write(`${line}
14286
14347
  `);
14287
14348
  }
14288
- async function pathExists(p) {
14289
- try {
14290
- await (0, import_promises10.stat)(p);
14291
- return true;
14292
- } catch {
14293
- return false;
14294
- }
14349
+ async function agentKey(path) {
14350
+ return await readSeed(path.dir) ?? (await openAgentState(path)).root;
14295
14351
  }
14296
14352
  async function runInit(opts) {
14297
14353
  const path = resolveStateDir({ cliFlag: opts.stateDir });
14298
- if (!opts.force && await pathExists(path.statePath)) {
14354
+ if (opts.force) await withStateLock(path.dir, () => import_node_fs7.promises.rm(path.statePath, { force: true }));
14355
+ const opened = await openAgentState(path);
14356
+ if (!opened.created) {
14299
14357
  logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
14300
- logStderr("Re-run with --force to overwrite (you will lose existing handles + identity).");
14358
+ logStderr("Re-run with --force to replace it with an empty one (its handles are lost; the address stays).");
14301
14359
  process.exit(2);
14302
14360
  }
14303
- const fromEnv = process.env[PASSPHRASE_ENV_VAR];
14304
- let passphrase;
14305
- if (fromEnv !== void 0 && fromEnv.length > 0) {
14306
- passphrase = fromEnv;
14307
- logStderr(
14308
- `${PROGRAM_NAME} init: using passphrase from ${PASSPHRASE_ENV_VAR} (will also save to OS keychain).`
14309
- );
14310
- } else {
14311
- passphrase = await promptHiddenPassphrase("Choose a passphrase (will not echo): ");
14312
- if (passphrase.length < MIN_PASSPHRASE_LEN) {
14313
- throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
14314
- }
14315
- const confirm = await promptHiddenPassphrase("Confirm passphrase: ");
14316
- if (confirm !== passphrase) {
14317
- throw new Error("Passphrases do not match.");
14318
- }
14319
- }
14320
- logStderr(`${PROGRAM_NAME} init: generating X25519 identity ...`);
14321
- const identity = await generateIdentity();
14322
- const identityFields = identityToFields(identity);
14323
- const state = createEmptyState(identityFields);
14324
- logStderr(`${PROGRAM_NAME} init: writing encrypted state to ${path.statePath} ...`);
14325
- const lock = await acquireStateLock(path.dir);
14326
- let keychainStored = false;
14327
- try {
14328
- if (!opts.force && await pathExists(path.statePath)) {
14329
- logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
14330
- logStderr("Re-run with --force to overwrite (you will lose existing handles + identity).");
14331
- process.exit(2);
14332
- }
14333
- await saveState(state, passphrase, path.statePath);
14334
- try {
14335
- await setKeychainPassphrase(passphrase);
14336
- keychainStored = true;
14337
- logStderr(
14338
- `${PROGRAM_NAME} init: passphrase stored in OS keychain (service=${KEYCHAIN_SERVICE} account=${KEYCHAIN_ACCOUNT}).`
14339
- );
14340
- } catch (err2) {
14341
- if (err2 instanceof KeytarUnavailableError) {
14342
- logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
14343
- logStderr(
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.`
14345
- );
14346
- } else {
14347
- throw err2;
14348
- }
14349
- }
14350
- } finally {
14351
- await lock.release();
14352
- }
14361
+ const identity = await identityFromSeed(opened.root.seed);
14353
14362
  logStdout(`${PROGRAM_NAME} init: ready.`);
14354
- logStdout(` state.json: ${path.statePath}`);
14355
- logStdout(` passphrase source: ${keychainStored ? "OS keychain" : `${PASSPHRASE_ENV_VAR} env`}`);
14356
- logStdout(` pubkey (base64): ${identityFields.x25519_public_b64}`);
14363
+ logStdout(` state.json: ${path.statePath}`);
14364
+ logStdout(` key source: ${opened.root.source}`);
14365
+ logStdout(` address: ${Buffer.from(identity.public).toString("base64url")}`);
14357
14366
  logStdout("");
14358
- logStdout(
14359
- "Share the pubkey with senders so they can target SEALs to your MCP server (see `seal send --to <pubkey>` from `seal-cli`)."
14360
- );
14361
- }
14362
- async function createStateWithoutPrompt(path) {
14363
- const resolved = await passphraseForNewState();
14364
- if (resolved === null) return null;
14365
- const lock = await acquireStateLock(path.dir);
14366
- try {
14367
- if (await pathExists(path.statePath)) return void 0;
14368
- if (resolved.fresh) await setKeychainPassphrase(resolved.passphrase);
14369
- const identity = await generateIdentity();
14370
- await saveState(createEmptyState(identityToFields(identity)), resolved.passphrase, path.statePath);
14371
- } finally {
14372
- await lock.release();
14373
- }
14374
- logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
14375
- return resolved;
14367
+ logStdout("Senders use the address to deliver a link only this agent opens (`seal send --to <address>`).");
14376
14368
  }
14377
14369
  async function stateForServe(path) {
14378
- if (!await pathExists(path.statePath)) {
14379
- const created = await createStateWithoutPrompt(path);
14380
- if (created !== void 0) return created;
14381
- }
14382
- const resolved = await resolvePassphraseFromStores();
14383
- if (resolved === null) {
14384
- throw new PassphraseNotFoundError();
14385
- }
14386
- if (resolved.source === "env") {
14387
- logStderr(
14388
- `${PROGRAM_NAME} serve: warning: passphrase read from ${PASSPHRASE_ENV_VAR}. Environment variables are readable via /proc/<pid>/environ on Linux and the process listing on Windows. Prefer the OS keychain (run \`${PROGRAM_NAME} init\`).`
14389
- );
14390
- }
14370
+ let opened;
14391
14371
  try {
14392
- const view = await loadState(resolved.passphrase, path.statePath);
14393
- await loadIdentity(view.identity);
14372
+ opened = await openAgentState(path);
14394
14373
  } catch (err2) {
14395
- if (err2 instanceof StatePassphraseInvalid) {
14396
- logStderr(`${PROGRAM_NAME} serve: passphrase rejected (AES-256-GCM tag check failed).`);
14397
- logStderr(
14398
- `If you reset your OS keychain, re-run \`${PROGRAM_NAME} init --force\` (you will lose existing handles).`
14399
- );
14374
+ if (err2 instanceof IdentityConflictError) {
14375
+ logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
14400
14376
  process.exit(3);
14401
14377
  }
14402
14378
  if (err2 instanceof StateCorrupt) {
@@ -14405,29 +14381,27 @@ async function stateForServe(path) {
14405
14381
  }
14406
14382
  throw err2;
14407
14383
  }
14408
- return resolved;
14384
+ if (opened.created) logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
14385
+ if (opened.moved !== null) logStderr(`${PROGRAM_NAME} serve: moved the 0.2 state under the agent's key (${opened.root.source})`);
14386
+ if (opened.moved === "env") {
14387
+ logStderr(
14388
+ `${PROGRAM_NAME} serve: ${PASSPHRASE_ENV_VAR} is no longer used; to move this agent to another machine, set ${SEED_ENV_VAR} there (\`${PROGRAM_NAME} seed --force\` prints it).`
14389
+ );
14390
+ }
14391
+ return opened;
14409
14392
  }
14410
14393
  async function runServe(opts) {
14411
14394
  const path = resolveStateDir({ cliFlag: opts.stateDir });
14412
- const resolved = opts.ephemeral ? null : await stateForServe(path);
14395
+ const opened = opts.ephemeral || process.env[EPHEMERAL_ENV_VAR] === "1" ? null : await stateForServe(path);
14413
14396
  logStderr(
14414
- resolved === null ? `${PROGRAM_NAME} serve: ephemeral \u2014 identity and handles live in memory until exit, nothing on disk` : `${PROGRAM_NAME} serve: state OK at ${path.statePath} (passphrase=${resolved.source})`
14397
+ opened === null ? `${PROGRAM_NAME} serve: ephemeral \u2014 a new key, identity and handles live in memory until exit, nothing on disk` : `${PROGRAM_NAME} serve: state OK at ${path.statePath} (key=${opened.root.source})`
14415
14398
  );
14416
14399
  logStderr(`${PROGRAM_NAME} serve: starting stdio MCP server ...`);
14417
- let handle;
14418
- try {
14419
- handle = await createServer({
14420
- state: resolved === null ? null : { path: path.statePath, passphrase: resolved.passphrase },
14421
- backendBaseUrl: opts.backendUrl,
14422
- publicHost: opts.publicHost
14423
- });
14424
- } catch (err2) {
14425
- if (err2 instanceof StateLockedError) {
14426
- logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
14427
- process.exit(5);
14428
- }
14429
- throw err2;
14430
- }
14400
+ const handle = await createServer({
14401
+ state: opened === null ? null : { path: path.statePath, passphrase: opened.passphrase },
14402
+ backendBaseUrl: opts.backendUrl,
14403
+ publicHost: opts.publicHost
14404
+ });
14431
14405
  const shutdown = async (signal) => {
14432
14406
  logStderr(`${PROGRAM_NAME} serve: received ${signal}, shutting down ...`);
14433
14407
  try {
@@ -14446,28 +14420,39 @@ async function runServe(opts) {
14446
14420
  });
14447
14421
  }
14448
14422
  async function runPubkey(opts) {
14449
- const path = resolveStateDir({ cliFlag: opts.stateDir });
14450
- if (!await pathExists(path.statePath)) {
14451
- throw new Error(`No state found at ${path.statePath}. Run \`${PROGRAM_NAME} init\` first.`);
14423
+ const root = await agentKey(resolveStateDir({ cliFlag: opts.stateDir }));
14424
+ const { public: address } = await identityFromSeed(root.seed);
14425
+ logStdout(Buffer.from(address).toString("base64url"));
14426
+ logStderr(`${keyEmoji(address)} key: ${root.source}`);
14427
+ }
14428
+ async function runSeed(opts) {
14429
+ if (!opts.force) {
14430
+ throw new Error(`seed prints the agent's secret key \u2014 re-run with --force to confirm, to set ${SEED_ENV_VAR} elsewhere.`);
14452
14431
  }
14453
- const resolved = await resolvePassphraseFromStores();
14454
- if (resolved === null) throw new PassphraseNotFoundError();
14455
- const view = await loadState(resolved.passphrase, path.statePath);
14456
- logStdout(view.identity.x25519_public_b64);
14432
+ const root = await agentKey(resolveStateDir({ cliFlag: opts.stateDir }));
14433
+ logStderr(
14434
+ `${PROGRAM_NAME} seed: this is the agent's secret key. Whoever holds it opens the links sent to this agent; put it only in ${SEED_ENV_VAR} of a machine you control.`
14435
+ );
14436
+ logStdout(Buffer.from(root.seed).toString("base64url"));
14437
+ }
14438
+ async function perms(file) {
14439
+ return (0, import_promises10.stat)(file).then(
14440
+ (st) => (st.mode & 511).toString(8).padStart(3, "0"),
14441
+ () => null
14442
+ );
14457
14443
  }
14458
14444
  async function runDoctor(opts) {
14459
14445
  const path = resolveStateDir({ cliFlag: opts.stateDir });
14460
- const envPass = process.env[PASSPHRASE_ENV_VAR];
14461
- let stateExists = false;
14462
- let statePerms = null;
14463
- let stateOwnerUid = null;
14446
+ const seedFile = (0, import_node_path11.join)(path.dir, SEED_FILE_NAME);
14447
+ let seedSource = null;
14448
+ let seedError = null;
14464
14449
  try {
14465
- const st = await (0, import_promises10.stat)(path.statePath);
14466
- stateExists = true;
14467
- statePerms = (st.mode & 511).toString(8).padStart(3, "0");
14468
- stateOwnerUid = typeof st.uid === "number" ? st.uid : null;
14469
- } catch {
14450
+ seedSource = (await readSeed(path.dir))?.source ?? null;
14451
+ } catch (err2) {
14452
+ seedError = err2 instanceof Error ? err2.message : String(err2);
14470
14453
  }
14454
+ const statePerms = await perms(path.statePath);
14455
+ const seedPerms = await perms(seedFile);
14471
14456
  const kc = await probeKeychain();
14472
14457
  const lockProbe = await probeStateLock(path.dir);
14473
14458
  const client = new SealApi({ baseUrl: resolveBackendUrl(opts.backendUrl) });
@@ -14488,103 +14473,101 @@ async function runDoctor(opts) {
14488
14473
  backendReachable = false;
14489
14474
  backendError = err2 instanceof Error ? err2.message : String(err2);
14490
14475
  }
14491
- const report = {
14492
- state_path: path.statePath,
14493
- state_exists: stateExists,
14494
- state_perms_octal: statePerms,
14495
- state_owner_uid: stateOwnerUid,
14496
- lock_held: lockProbe.held,
14497
- lock_holder_pid: lockProbe.holder?.pid ?? null,
14498
- lock_stale: lockProbe.stale,
14499
- keychain_available: kc.available,
14500
- keychain_has_entry: kc.hasEntry,
14501
- env_passphrase_set: envPass !== void 0 && envPass.length > 0,
14502
- backend_url: client.baseUrl,
14503
- public_host: resolvePublicHost(opts.publicHost),
14504
- backend_reachable: backendReachable,
14505
- backend_status: backendStatus,
14506
- backend_error: backendError,
14507
- platform: process.platform,
14508
- node_version: process.version
14509
- };
14510
- logStdout(JSON.stringify(report, null, 2));
14511
- if (!stateExists) {
14512
- logStderr(`hint: no state.json yet \u2014 the first \`${PROGRAM_NAME} serve\` creates it.`);
14513
- } else if (statePerms !== "600") {
14514
- logStderr(`hint: state.json perms are ${statePerms}, expected 600. Run \`chmod 600 \u2026\`.`);
14476
+ logStdout(
14477
+ JSON.stringify(
14478
+ {
14479
+ state_path: path.statePath,
14480
+ state_exists: statePerms !== null,
14481
+ state_perms_octal: statePerms,
14482
+ seed_source: seedSource,
14483
+ seed_error: seedError,
14484
+ seed_file_perms_octal: seedPerms,
14485
+ keychain_available: kc.available,
14486
+ keychain_has_seed: kc.hasSeed,
14487
+ env_seed_set: Boolean(process.env[SEED_ENV_VAR]),
14488
+ env_passphrase_set: Boolean(process.env[PASSPHRASE_ENV_VAR]),
14489
+ lock_held: lockProbe.held,
14490
+ lock_holder_pid: lockProbe.holder?.pid ?? null,
14491
+ lock_stale: lockProbe.stale,
14492
+ backend_url: client.baseUrl,
14493
+ public_host: resolvePublicHost(opts.publicHost),
14494
+ backend_reachable: backendReachable,
14495
+ backend_status: backendStatus,
14496
+ backend_error: backendError,
14497
+ platform: process.platform,
14498
+ node_version: process.version
14499
+ },
14500
+ null,
14501
+ 2
14502
+ )
14503
+ );
14504
+ if (seedError !== null) logStderr(`hint: ${seedError}`);
14505
+ else if (seedSource === null) {
14506
+ logStderr(`hint: no agent key yet \u2014 the first \`${PROGRAM_NAME} serve\` makes it (${kc.available ? "in the OS keychain" : seedFile}).`);
14515
14507
  }
14516
- if (!kc.available) {
14517
- logStderr(
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).`
14519
- );
14520
- } else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
14521
- logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
14508
+ for (const [file, got] of [[path.statePath, statePerms], [seedFile, seedPerms]]) {
14509
+ if (got !== null && got !== "600") logStderr(`hint: ${file} perms are ${got}, expected 600. Run \`chmod 600 \u2026\`.`);
14522
14510
  }
14523
14511
  if (backendReachable === false) {
14524
14512
  logStderr(`hint: backend ${client.baseUrl} unreachable (${backendError ?? "unknown"}).`);
14525
14513
  }
14526
- if (lockProbe.held && !lockProbe.stale) {
14527
- logStderr(
14528
- `hint: state dir is locked by pid ${lockProbe.holder?.pid ?? "?"} \u2014 a serve instance is running.`
14529
- );
14530
- } else if (lockProbe.held && lockProbe.stale) {
14531
- logStderr("hint: stale state.lock detected (holder dead) \u2014 next serve/init will break it automatically.");
14514
+ if (lockProbe.held && lockProbe.stale) {
14515
+ logStderr("hint: stale state.lock detected (holder dead) \u2014 the next write breaks it automatically.");
14532
14516
  }
14533
14517
  }
14534
14518
  async function runReset(opts) {
14535
14519
  if (!opts.force) {
14536
14520
  throw new Error(
14537
- "reset is destructive \u2014 re-run with --force to confirm. This wipes state.json AND your keychain passphrase entry."
14521
+ "reset is destructive \u2014 re-run with --force to confirm. This wipes state.json AND the agent's key: its address is lost for good."
14538
14522
  );
14539
14523
  }
14540
14524
  const path = resolveStateDir({ cliFlag: opts.stateDir });
14541
- const lock = await acquireStateLock(path.dir);
14542
- let stateDeleted = false;
14543
- let keychainDeleted = false;
14544
- try {
14545
- try {
14546
- await import_node_fs6.promises.unlink(path.statePath);
14547
- stateDeleted = true;
14548
- } catch (err2) {
14549
- const code = err2.code;
14550
- if (code !== "ENOENT") {
14551
- throw err2;
14552
- }
14553
- }
14554
- keychainDeleted = await deleteKeychainPassphrase();
14555
- } finally {
14556
- await lock.release();
14557
- }
14525
+ const result = await withStateLock(path.dir, async () => {
14526
+ let stateDeleted = true;
14527
+ await import_node_fs7.promises.unlink(path.statePath).catch((err2) => {
14528
+ if (err2.code !== "ENOENT") throw err2;
14529
+ stateDeleted = false;
14530
+ });
14531
+ return { stateDeleted, ...await deleteRootSecret(path.dir) };
14532
+ });
14558
14533
  logStdout(
14559
14534
  JSON.stringify(
14560
14535
  {
14561
- state_deleted: stateDeleted,
14562
- keychain_deleted: keychainDeleted,
14536
+ state_deleted: result.stateDeleted,
14537
+ seed_file_deleted: result.seedFile,
14538
+ keychain_deleted: result.keychain,
14563
14539
  state_path: path.statePath
14564
14540
  },
14565
14541
  null,
14566
14542
  2
14567
14543
  )
14568
14544
  );
14545
+ logStderr(
14546
+ `${PROGRAM_NAME} reset: the agent's address is gone for good \u2014 links sent to it can no longer be opened, and whoever sends to this agent needs its new address.`
14547
+ );
14548
+ if (process.env[SEED_ENV_VAR]) {
14549
+ logStderr(`${PROGRAM_NAME} reset: ${SEED_ENV_VAR} is still set: the next start uses the key from the environment.`);
14550
+ }
14569
14551
  }
14570
14552
  function buildCli() {
14571
14553
  const program = new import_commander.Command();
14572
14554
  program.name(PROGRAM_NAME).description(DESCRIPTION).version(SERVER_VERSION, "-V, --version", `Print the ${PROGRAM_NAME} version and exit.`);
14573
14555
  program.command("init").description(
14574
- "Optional setup with a passphrase you choose: generate X25519 identity, store the passphrase in OS keychain, and write encrypted state.json (`serve` does this itself with a random passphrase when there is no state)."
14575
- ).option("--state-dir <path>", "Override default state directory").option("--force", "Overwrite an existing state.json (DESTRUCTIVE)", false).action(async (opts) => runInit(opts));
14556
+ "Make the agent's key (SEAL_SEED, else the OS keychain, else a 0600 file) and an empty encrypted state.json, without starting the server (`serve` does this itself on its first start)."
14557
+ ).option("--state-dir <path>", "Override default state directory").option("--force", "Replace an existing state.json with an empty one (DESTRUCTIVE; the address stays)", false).action(async (opts) => runInit(opts));
14576
14558
  program.command("serve", { isDefault: true }).description(`Start the stdio MCP server (DEFAULT \u2014 bare \`${PROGRAM_NAME}\` runs this).`).option("--state-dir <path>", "Override default state directory").option("--backend-url <url>", "SEAL backend base URL (overrides SEAL_MCP_BACKEND_URL; default https://api.seal.net)").option(
14577
14559
  "--public-host <url>",
14578
14560
  "Public origin for minted share URLs (overrides SEAL_MCP_PUBLIC_HOST; default https://seal.net). Set BOTH this and --backend-url for self-hosted deployments."
14579
- ).option("--ephemeral", "Keep identity and handles in memory only; write nothing to disk", false).action(async (opts) => runServe(opts));
14561
+ ).option("--ephemeral", `A new key each start, handles in memory only, nothing on disk (also ${EPHEMERAL_ENV_VAR}=1)`, false).action(async (opts) => runServe(opts));
14580
14562
  program.command("pubkey").description(
14581
- "Print this server's X25519 public key (base64). Senders use it as the `--to` argument of `seal send` to deliver targeted SEALs."
14563
+ "Print the agent's address, its X25519 public key (base64url), on stdout; its emoji and the key's source on stderr. Senders use it as the `--to` of `seal send` to deliver targeted SEALs."
14582
14564
  ).option("--state-dir <path>", "Override default state directory").action(async (opts) => runPubkey(opts));
14565
+ program.command("seed").description(`Print the agent's secret key, to give another machine the same address through ${SEED_ENV_VAR}.`).option("--state-dir <path>", "Override default state directory").option("--force", "Confirm printing the secret key", false).action(async (opts) => runSeed(opts));
14583
14566
  program.command("doctor").description(
14584
- "Diagnose keychain availability, state-file permissions, env-var presence, and backend reachability. Prints a JSON report to stdout + hints to stderr."
14567
+ "Diagnose the agent's key, keychain availability, file permissions and backend reachability. Prints a JSON report to stdout + hints to stderr."
14585
14568
  ).option("--state-dir <path>", "Override default state directory").option("--backend-url <url>", "Backend URL to probe").option("--public-host <url>", "Public share-URL origin to report (overrides SEAL_MCP_PUBLIC_HOST)").action(async (opts) => runDoctor(opts));
14586
14569
  program.command("reset").description(
14587
- "Wipe state.json + keychain passphrase entry. Use only after passphrase loss; destroys all locally-tracked handles and the X25519 identity."
14570
+ "Wipe state.json and the agent's key (seed file and keychain entries): destroys all locally-tracked handles, and the address is lost for good."
14588
14571
  ).option("--state-dir <path>", "Override default state directory").option("--force", "Confirm destructive operation", false).action(async (opts) => runReset(opts));
14589
14572
  return program;
14590
14573
  }