sealnet-mcp 0.2.5 → 0.4.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/README.md +22 -18
- package/dist/cli.cjs +1246 -943
- package/dist/cli.d.cts +10 -7
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +5719 -5416
- package/dist/index.cjs +706 -305
- package/dist/index.d.cts +219 -110
- package/dist/index.d.ts +219 -110
- package/dist/index.js +684 -291
- package/package.json +1 -1
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
|
|
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
|
|
@@ -2889,9 +2890,60 @@ var limits_default = {
|
|
|
2889
2890
|
INTAKE_TTL_DEFAULT_SECONDS: 900,
|
|
2890
2891
|
INTAKE_TTL_MAX_SECONDS: 86400,
|
|
2891
2892
|
INTAKE_WAIT_MAX_SECONDS: 25,
|
|
2892
|
-
PAYMENT_WAIT_MAX_SECONDS: 82800
|
|
2893
|
+
PAYMENT_WAIT_MAX_SECONDS: 82800,
|
|
2894
|
+
ADDRESS_PROOF_TTL_SECONDS: 300,
|
|
2895
|
+
INBOX_MAX_ITEMS: 200
|
|
2893
2896
|
}
|
|
2894
2897
|
};
|
|
2898
|
+
var seal_address_v1_default = {
|
|
2899
|
+
version: 1,
|
|
2900
|
+
$comment: "Address of a key: who a seal is for, without a URL (audit/SPEC-ADDRESS.md). Numbers live in limits.json, error codes in error_codes.json.",
|
|
2901
|
+
address: {
|
|
2902
|
+
key: "X25519 public key, 32 bytes",
|
|
2903
|
+
text: "base64url without padding, 43 characters",
|
|
2904
|
+
emoji: "fingerprint_emoji.json over sha256(key)[0..3], the same picture as intake",
|
|
2905
|
+
inbox_hash: "sha256(key), 32 bytes: the server finds an inbox by it"
|
|
2906
|
+
},
|
|
2907
|
+
seed: {
|
|
2908
|
+
env: "SEAL_SEED",
|
|
2909
|
+
text: "base64url without padding of the 32-byte X25519 secret, 43 characters",
|
|
2910
|
+
same_as: [
|
|
2911
|
+
"sealnet-mcp state.identity.x25519_secret_b64",
|
|
2912
|
+
"seal identity.v1.json x25519_secret_b64url",
|
|
2913
|
+
"sealnet (npm) KeyPair.secretKey",
|
|
2914
|
+
"sealnet (PyPI) keygen().secret_key"
|
|
2915
|
+
]
|
|
2916
|
+
},
|
|
2917
|
+
scopes: {
|
|
2918
|
+
share_link: "seal_share",
|
|
2919
|
+
owner_copy: "seal_owner",
|
|
2920
|
+
address_proof: "seal_address_proof"
|
|
2921
|
+
},
|
|
2922
|
+
proof: {
|
|
2923
|
+
header_address: "X-Seal-Address",
|
|
2924
|
+
header_proof: "X-Seal-Proof",
|
|
2925
|
+
proof_text: "<ts>.<token>: ts is the decimal Unix second from the challenge, token base64url of 32 bytes",
|
|
2926
|
+
challenge: "sealed box of the 32-byte token to the address with scope (address_proof, decimal ts): ephemeral_pub(32) || nonce(12) || ciphertext(48), base64url, 123 characters",
|
|
2927
|
+
token: "opaque to clients: unwrap it and send it back"
|
|
2928
|
+
},
|
|
2929
|
+
items: {
|
|
2930
|
+
delivery: {
|
|
2931
|
+
id: "s.<seal_id>.<share_code>",
|
|
2932
|
+
is: "a share link addressed to this key; fragment = its key wrapped to the address (x25519:, targeted_x25519_v1, scope share_link)"
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
request_kinds: ["secret", "file", "any"],
|
|
2936
|
+
vectors: [
|
|
2937
|
+
{
|
|
2938
|
+
comment: "generated once by backend_crypto.sealed_box.seal_key_for_recipient; every client must unwrap it",
|
|
2939
|
+
secret: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc",
|
|
2940
|
+
address: "E75P6uryBMf9M1j8nAByGIHRdCeBKCJ-xnTzf3_pe20",
|
|
2941
|
+
ts: 179e7,
|
|
2942
|
+
challenge: "4Oel-UMfgP_1dBhmf13yZiujhPDouCIQ6sSx1nY1qy-vyzFqDqb9jARbJ0e5A8vv4EDELDS5OPqBQMcsmDA-C_lXO95y_CVUyYtSu-1fltsy5ZihOFDp0T-C9Ro",
|
|
2943
|
+
token: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"
|
|
2944
|
+
}
|
|
2945
|
+
]
|
|
2946
|
+
};
|
|
2895
2947
|
var upload_default = {
|
|
2896
2948
|
version: 1,
|
|
2897
2949
|
upload: {
|
|
@@ -3223,13 +3275,24 @@ var SealApi = class _SealApi {
|
|
|
3223
3275
|
});
|
|
3224
3276
|
return { etag: resp.headers.get("ETag") };
|
|
3225
3277
|
}
|
|
3226
|
-
/**
|
|
3278
|
+
/**
|
|
3279
|
+
* `POST /seal/{id}/share` — mint a share link, returns its code. With an
|
|
3280
|
+
* address the link also lands in that key's inbox, in the same request.
|
|
3281
|
+
*/
|
|
3227
3282
|
async createShareLink(sealId, ownerToken, encryptedBlobB64, opts = {}) {
|
|
3283
|
+
const headers = _SealApi.ownerHeaders(ownerToken, true);
|
|
3284
|
+
const body = { encrypted_blob: encryptedBlobB64 };
|
|
3285
|
+
if ("to" in opts) {
|
|
3286
|
+
body.to = opts.to;
|
|
3287
|
+
body.fragment = opts.fragment;
|
|
3288
|
+
if (opts.label) body.label = opts.label;
|
|
3289
|
+
if (opts.sender) Object.assign(headers, proofHeaders(opts.sender));
|
|
3290
|
+
}
|
|
3228
3291
|
const resp = await this.request(`/seal/${encode(sealId)}/share`, {
|
|
3229
3292
|
what: `POST /seal/${sealId}/share`,
|
|
3230
3293
|
method: "POST",
|
|
3231
|
-
headers
|
|
3232
|
-
body: JSON.stringify(
|
|
3294
|
+
headers,
|
|
3295
|
+
body: JSON.stringify(body),
|
|
3233
3296
|
signal: opts.signal
|
|
3234
3297
|
});
|
|
3235
3298
|
const data = await resp.json();
|
|
@@ -3412,8 +3475,57 @@ var SealApi = class _SealApi {
|
|
|
3412
3475
|
});
|
|
3413
3476
|
return await resp.json();
|
|
3414
3477
|
}
|
|
3478
|
+
// ---------------------------------------------------------------------------
|
|
3479
|
+
// Inbox (SPEC-ADDRESS F2): the share links addressed to a key
|
|
3480
|
+
// ---------------------------------------------------------------------------
|
|
3481
|
+
/** `POST /intake/proof` — a challenge sealed to `address`. */
|
|
3482
|
+
async requestAddressProof(address, opts = {}) {
|
|
3483
|
+
const resp = await this.request("/intake/proof", {
|
|
3484
|
+
what: "POST /intake/proof",
|
|
3485
|
+
method: "POST",
|
|
3486
|
+
headers: { "Content-Type": "application/json" },
|
|
3487
|
+
body: JSON.stringify({ address }),
|
|
3488
|
+
signal: opts.signal
|
|
3489
|
+
});
|
|
3490
|
+
return await resp.json();
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* `GET /intake?to=` — every live item. `waitSeconds` holds the answer
|
|
3494
|
+
* while the inbox is empty, or, with `seen`, while its version is still
|
|
3495
|
+
* `seen`; one call waits at most `INTAKE_WAIT_MAX_SECONDS`, longer waits
|
|
3496
|
+
* are the caller's loop.
|
|
3497
|
+
*/
|
|
3498
|
+
async listInbox(proof, opts = {}) {
|
|
3499
|
+
const params = new URLSearchParams({ to: proof.address });
|
|
3500
|
+
if (opts.waitSeconds) {
|
|
3501
|
+
params.set("wait", String(Math.min(opts.waitSeconds, limits_default.limits.INTAKE_WAIT_MAX_SECONDS)));
|
|
3502
|
+
}
|
|
3503
|
+
if (opts.seen) params.set("seen", opts.seen);
|
|
3504
|
+
const resp = await this.request(`/intake?${params.toString()}`, {
|
|
3505
|
+
what: "GET /intake",
|
|
3506
|
+
headers: proofHeaders(proof),
|
|
3507
|
+
signal: opts.signal
|
|
3508
|
+
});
|
|
3509
|
+
return await resp.json();
|
|
3510
|
+
}
|
|
3511
|
+
/** `DELETE /intake?to=&item=` — take one item out; the link keeps working. */
|
|
3512
|
+
async dismissInboxItem(itemId, proof, opts = {}) {
|
|
3513
|
+
const params = new URLSearchParams({ to: proof.address, item: itemId });
|
|
3514
|
+
await this.request(`/intake?${params.toString()}`, {
|
|
3515
|
+
what: "DELETE /intake",
|
|
3516
|
+
method: "DELETE",
|
|
3517
|
+
headers: proofHeaders(proof),
|
|
3518
|
+
signal: opts.signal
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3415
3521
|
};
|
|
3416
3522
|
var NOT_READY_DEFAULT_RETRY_AFTER_S = 3;
|
|
3523
|
+
function proofHeaders(proof) {
|
|
3524
|
+
return {
|
|
3525
|
+
[seal_address_v1_default.proof.header_address]: proof.address,
|
|
3526
|
+
[seal_address_v1_default.proof.header_proof]: proof.proof
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3417
3529
|
function abortableSleep(ms, signal) {
|
|
3418
3530
|
return new Promise((resolve5, reject) => {
|
|
3419
3531
|
if (signal?.aborted) {
|
|
@@ -6178,6 +6290,7 @@ function parseUrlType(pathname) {
|
|
|
6178
6290
|
var TARGETED_PREFIX_V1 = "x25519:";
|
|
6179
6291
|
var TARGETED_PREFIX_V2_RESERVED = "x25519v2:";
|
|
6180
6292
|
var SCOPE_TYPE = "seal_share";
|
|
6293
|
+
var ADDRESS_PROOF_SCOPE = "seal_address_proof";
|
|
6181
6294
|
var X25519_KEY_BYTES = 32;
|
|
6182
6295
|
var LINK_KEY_BYTES2 = 32;
|
|
6183
6296
|
var PACKED_BYTES_V1 = 92;
|
|
@@ -6284,6 +6397,11 @@ async function unwrapTargetedFragment(fragment, recipientSecret, sealId) {
|
|
|
6284
6397
|
}
|
|
6285
6398
|
return linkKey;
|
|
6286
6399
|
}
|
|
6400
|
+
async function proveAddress(secret, challenge, ts) {
|
|
6401
|
+
const wasm3 = await ensureWasm();
|
|
6402
|
+
const token = wasm3.wasm_sealbox_unwrap(base64urlToBytes(challenge), secret, ADDRESS_PROOF_SCOPE, String(ts));
|
|
6403
|
+
return `${ts}.${bytesToBase64url(token)}`;
|
|
6404
|
+
}
|
|
6287
6405
|
var fingerprint_emoji_default = {
|
|
6288
6406
|
version: 1,
|
|
6289
6407
|
name: "FINGERPRINT_EMOJI",
|
|
@@ -6743,6 +6861,36 @@ async function waitForIntake(api, intakeId, opts = {}) {
|
|
|
6743
6861
|
if (view.status === "fulfilled" && view.share_path) return view.share_path;
|
|
6744
6862
|
}
|
|
6745
6863
|
}
|
|
6864
|
+
var PROOF_MARGIN_MS = 3e4;
|
|
6865
|
+
var proofs = /* @__PURE__ */ new Map();
|
|
6866
|
+
async function addressProof(api, secret, opts = {}) {
|
|
6867
|
+
const address = bytesToBase64url(await recipientPublicKey(secret));
|
|
6868
|
+
const key = `${api.baseUrl} ${address}`;
|
|
6869
|
+
const kept = proofs.get(key);
|
|
6870
|
+
if (!opts.fresh && kept && kept.until > Date.now()) return kept.proof;
|
|
6871
|
+
const challenge = await api.requestAddressProof(address, opts);
|
|
6872
|
+
const proof = { address, proof: await proveAddress(secret, challenge.challenge, challenge.ts) };
|
|
6873
|
+
proofs.set(key, { proof, until: Date.parse(challenge.expires_at) - PROOF_MARGIN_MS });
|
|
6874
|
+
return proof;
|
|
6875
|
+
}
|
|
6876
|
+
async function withAddressProof(api, secret, signal, call) {
|
|
6877
|
+
const request = signal ? { signal } : {};
|
|
6878
|
+
try {
|
|
6879
|
+
return await call(await addressProof(api, secret, request));
|
|
6880
|
+
} catch (err2) {
|
|
6881
|
+
if (!(err2 instanceof BackendError && err2.appCode === "intake.proof_required")) throw err2;
|
|
6882
|
+
return call(await addressProof(api, secret, { ...request, fresh: true }));
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
async function readInbox(api, secret, opts = {}) {
|
|
6886
|
+
return withAddressProof(api, secret, opts.signal, (proof) => api.listInbox(proof, opts));
|
|
6887
|
+
}
|
|
6888
|
+
async function removeFromInbox(api, secret, itemId, opts = {}) {
|
|
6889
|
+
return withAddressProof(api, secret, opts.signal, (proof) => api.dismissInboxItem(itemId, proof, opts));
|
|
6890
|
+
}
|
|
6891
|
+
function inboxItemUrl(site, item) {
|
|
6892
|
+
return `${site.replace(/\/+$/, "")}${item.path}#${item.fragment}`;
|
|
6893
|
+
}
|
|
6746
6894
|
async function describeFile(file, fileKeys) {
|
|
6747
6895
|
const keyB64 = file.sha256_hex ? fileKeys[file.sha256_hex] : void 0;
|
|
6748
6896
|
const base = { id: file.id, size: file.size, sha256Hex: file.sha256_hex };
|
|
@@ -6901,6 +7049,9 @@ async function generateIdentity() {
|
|
|
6901
7049
|
const { publicKey, secretKey } = await generateRecipientKeyPair();
|
|
6902
7050
|
return { secret: secretKey, public: publicKey };
|
|
6903
7051
|
}
|
|
7052
|
+
async function identityFromSeed(seed) {
|
|
7053
|
+
return { secret: seed, public: await recipientPublicKey(seed) };
|
|
7054
|
+
}
|
|
6904
7055
|
function identityToFields(id) {
|
|
6905
7056
|
return {
|
|
6906
7057
|
x25519_secret_b64: toB64(id.secret),
|
|
@@ -6934,20 +7085,27 @@ var import_node_crypto = require("crypto");
|
|
|
6934
7085
|
var import_node_os = require("os");
|
|
6935
7086
|
var import_node_path = require("path");
|
|
6936
7087
|
var LOCK_FILE_NAME = "state.lock";
|
|
7088
|
+
var LOCK_WAIT_MS = 5e3;
|
|
7089
|
+
var LOCK_RETRY_MS = 25;
|
|
6937
7090
|
var StateLockedError = class extends Error {
|
|
6938
7091
|
code = "state_locked";
|
|
6939
7092
|
holder;
|
|
6940
7093
|
lockPath;
|
|
6941
7094
|
constructor(lockPath, holder) {
|
|
6942
7095
|
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
|
-
);
|
|
7096
|
+
super(`state directory is locked by another sealnet-mcp process (${who}). Lock file: ${lockPath}`);
|
|
6946
7097
|
this.name = "StateLockedError";
|
|
6947
7098
|
this.holder = holder;
|
|
6948
7099
|
this.lockPath = lockPath;
|
|
6949
7100
|
}
|
|
6950
7101
|
};
|
|
7102
|
+
var StateBusyError = class extends Error {
|
|
7103
|
+
code = "state_busy";
|
|
7104
|
+
constructor(locked) {
|
|
7105
|
+
super(`${locked.message}; waited ${LOCK_WAIT_MS / 1e3} s, nothing was changed, safe to retry`);
|
|
7106
|
+
this.name = "StateBusyError";
|
|
7107
|
+
}
|
|
7108
|
+
};
|
|
6951
7109
|
var StateDirUnsafeError = class extends Error {
|
|
6952
7110
|
code = "state_dir_unsafe";
|
|
6953
7111
|
constructor(stateDir, reason) {
|
|
@@ -6966,6 +7124,9 @@ function defaultIsPidAlive(pid) {
|
|
|
6966
7124
|
return code === "EPERM";
|
|
6967
7125
|
}
|
|
6968
7126
|
}
|
|
7127
|
+
function holderMayRun(holder, deps = {}) {
|
|
7128
|
+
return holder.hostname !== (deps.host ?? (0, import_node_os.hostname)()) || (deps.isPidAlive ?? defaultIsPidAlive)(holder.pid);
|
|
7129
|
+
}
|
|
6969
7130
|
function parseHolder(raw) {
|
|
6970
7131
|
try {
|
|
6971
7132
|
const parsed = JSON.parse(raw);
|
|
@@ -7008,7 +7169,7 @@ async function tryCreate(lockPath, info) {
|
|
|
7008
7169
|
await (0, import_promises.unlink)(tmp).catch(() => void 0);
|
|
7009
7170
|
}
|
|
7010
7171
|
}
|
|
7011
|
-
async function tryBreakStale(lockPath, probed,
|
|
7172
|
+
async function tryBreakStale(lockPath, probed, deps) {
|
|
7012
7173
|
const claimPath = `${lockPath}.break-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}`;
|
|
7013
7174
|
try {
|
|
7014
7175
|
await (0, import_promises.rename)(lockPath, claimPath);
|
|
@@ -7016,8 +7177,7 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
7016
7177
|
return true;
|
|
7017
7178
|
}
|
|
7018
7179
|
const claimed = parseHolder(await (0, import_promises.readFile)(claimPath, "utf8").catch(() => ""));
|
|
7019
|
-
|
|
7020
|
-
if (sameHolder(claimed, probed) || claimedStale) {
|
|
7180
|
+
if (sameHolder(claimed, probed) || claimed === null || !holderMayRun(claimed, deps)) {
|
|
7021
7181
|
await (0, import_promises.unlink)(claimPath).catch(() => void 0);
|
|
7022
7182
|
return true;
|
|
7023
7183
|
}
|
|
@@ -7030,11 +7190,9 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
7030
7190
|
}
|
|
7031
7191
|
async function acquireStateLock(stateDir, deps = {}) {
|
|
7032
7192
|
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
7193
|
const self2 = {
|
|
7036
7194
|
pid: deps.pid ?? process.pid,
|
|
7037
|
-
hostname:
|
|
7195
|
+
hostname: deps.host ?? (0, import_node_os.hostname)(),
|
|
7038
7196
|
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7039
7197
|
};
|
|
7040
7198
|
await (0, import_promises.mkdir)(stateDir, { recursive: true, mode: 448 });
|
|
@@ -7045,11 +7203,10 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
7045
7203
|
await (0, import_promises.chmod)(stateDir, 448).catch(() => void 0);
|
|
7046
7204
|
if (!await tryCreate(lockPath, self2)) {
|
|
7047
7205
|
const holder = await readHolder(lockPath);
|
|
7048
|
-
|
|
7049
|
-
if (holder !== null && !stale) {
|
|
7206
|
+
if (holder !== null && holderMayRun(holder, deps)) {
|
|
7050
7207
|
throw new StateLockedError(lockPath, holder);
|
|
7051
7208
|
}
|
|
7052
|
-
if (!await tryBreakStale(lockPath, holder,
|
|
7209
|
+
if (!await tryBreakStale(lockPath, holder, deps)) {
|
|
7053
7210
|
throw new StateLockedError(lockPath, await readHolder(lockPath));
|
|
7054
7211
|
}
|
|
7055
7212
|
if (!await tryCreate(lockPath, self2)) {
|
|
@@ -7081,6 +7238,30 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
7081
7238
|
}
|
|
7082
7239
|
};
|
|
7083
7240
|
}
|
|
7241
|
+
var queues = /* @__PURE__ */ new Map();
|
|
7242
|
+
function withStateLock(stateDir, fn, deps = {}) {
|
|
7243
|
+
const run = (queues.get(stateDir) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
7244
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
7245
|
+
let lock;
|
|
7246
|
+
for (; ; ) {
|
|
7247
|
+
try {
|
|
7248
|
+
lock = await acquireStateLock(stateDir, deps);
|
|
7249
|
+
break;
|
|
7250
|
+
} catch (err2) {
|
|
7251
|
+
if (!(err2 instanceof StateLockedError)) throw err2;
|
|
7252
|
+
if (Date.now() >= deadline) throw new StateBusyError(err2);
|
|
7253
|
+
await new Promise((resolve5) => setTimeout(resolve5, LOCK_RETRY_MS));
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
try {
|
|
7257
|
+
return await fn();
|
|
7258
|
+
} finally {
|
|
7259
|
+
await lock.release();
|
|
7260
|
+
}
|
|
7261
|
+
});
|
|
7262
|
+
queues.set(stateDir, run);
|
|
7263
|
+
return run;
|
|
7264
|
+
}
|
|
7084
7265
|
async function probeStateLock(stateDir, deps = {}) {
|
|
7085
7266
|
const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
|
|
7086
7267
|
const holder = await readHolder(lockPath);
|
|
@@ -7092,182 +7273,7 @@ async function probeStateLock(stateDir, deps = {}) {
|
|
|
7092
7273
|
return { held: false, holder: null, stale: false };
|
|
7093
7274
|
}
|
|
7094
7275
|
}
|
|
7095
|
-
|
|
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
|
-
});
|
|
7276
|
+
return { held: true, holder, stale: !holderMayRun(holder, deps) };
|
|
7271
7277
|
}
|
|
7272
7278
|
|
|
7273
7279
|
// src/paths.ts
|
|
@@ -7310,99 +7316,533 @@ function osDefaultDir(env, platform, home) {
|
|
|
7310
7316
|
}
|
|
7311
7317
|
}
|
|
7312
7318
|
|
|
7313
|
-
// src/
|
|
7314
|
-
var
|
|
7315
|
-
var
|
|
7316
|
-
var
|
|
7319
|
+
// src/seed.ts
|
|
7320
|
+
var import_node_child_process = require("child_process");
|
|
7321
|
+
var import_node_crypto3 = require("crypto");
|
|
7322
|
+
var import_node_fs3 = require("fs");
|
|
7323
|
+
var import_node_path4 = require("path");
|
|
7317
7324
|
|
|
7318
|
-
//
|
|
7319
|
-
var
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7325
|
+
// src/state.ts
|
|
7326
|
+
var import_node_crypto2 = require("crypto");
|
|
7327
|
+
var import_node_fs2 = require("fs");
|
|
7328
|
+
var import_node_path3 = require("path");
|
|
7329
|
+
var import_argon2 = __toESM(require("argon2"), 1);
|
|
7330
|
+
var FORMAT_TAG = "SEAL_MCP_STATE v1";
|
|
7331
|
+
var STATE_VERSION = 1;
|
|
7332
|
+
var ARGON2_MEMORY_KIB = 65536;
|
|
7333
|
+
var ARGON2_ITERATIONS = 3;
|
|
7334
|
+
var ARGON2_PARALLELISM = 4;
|
|
7335
|
+
var ARGON2_SALT_BYTES = 16;
|
|
7336
|
+
var KDF_BOUNDS = {
|
|
7337
|
+
memory_kib: { min: 8192, max: 1048576 },
|
|
7338
|
+
// 8 MiB .. 1 GiB
|
|
7339
|
+
iterations: { min: 1, max: 16 },
|
|
7340
|
+
parallelism: { min: 1, max: 16 }
|
|
7341
|
+
};
|
|
7342
|
+
var AES_KEY_BYTES = 32;
|
|
7343
|
+
var AES_NONCE_BYTES = 12;
|
|
7344
|
+
var StatePassphraseInvalid = class extends Error {
|
|
7345
|
+
constructor() {
|
|
7346
|
+
super("AES-256-GCM tag check failed: another passphrase, or a tampered file");
|
|
7347
|
+
this.name = "StatePassphraseInvalid";
|
|
7348
|
+
}
|
|
7349
|
+
};
|
|
7350
|
+
var StateCorrupt = class extends Error {
|
|
7351
|
+
constructor(reason) {
|
|
7352
|
+
super(`state.corrupt: ${reason}`);
|
|
7353
|
+
this.name = "StateCorrupt";
|
|
7354
|
+
}
|
|
7355
|
+
};
|
|
7356
|
+
function toB642(bytes) {
|
|
7357
|
+
return Buffer.from(bytes).toString("base64");
|
|
7358
|
+
}
|
|
7359
|
+
function fromB642(s) {
|
|
7360
|
+
return new Uint8Array(Buffer.from(s, "base64"));
|
|
7361
|
+
}
|
|
7362
|
+
var CURRENT_KDF_PARAMS = {
|
|
7363
|
+
memory_kib: ARGON2_MEMORY_KIB,
|
|
7364
|
+
iterations: ARGON2_ITERATIONS,
|
|
7365
|
+
parallelism: ARGON2_PARALLELISM
|
|
7366
|
+
};
|
|
7367
|
+
function boundedKdfParam(name, value) {
|
|
7368
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
7369
|
+
throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
|
|
7370
|
+
}
|
|
7371
|
+
const { min, max } = KDF_BOUNDS[name];
|
|
7372
|
+
if (value < min || value > max) {
|
|
7373
|
+
throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
|
|
7374
|
+
}
|
|
7375
|
+
return value;
|
|
7376
|
+
}
|
|
7377
|
+
async function deriveKek(passphrase, saltB64, params) {
|
|
7378
|
+
const salt = fromB642(saltB64);
|
|
7379
|
+
if (salt.length !== ARGON2_SALT_BYTES) {
|
|
7380
|
+
throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
|
|
7381
|
+
}
|
|
7382
|
+
const kek = await import_argon2.default.hash(passphrase, {
|
|
7383
|
+
type: import_argon2.default.argon2id,
|
|
7384
|
+
raw: true,
|
|
7385
|
+
salt: Buffer.from(salt),
|
|
7386
|
+
memoryCost: params.memory_kib,
|
|
7387
|
+
timeCost: params.iterations,
|
|
7388
|
+
parallelism: params.parallelism,
|
|
7389
|
+
hashLength: AES_KEY_BYTES
|
|
7390
|
+
});
|
|
7391
|
+
return new Uint8Array(kek);
|
|
7392
|
+
}
|
|
7393
|
+
async function aesGcmEncrypt(kek, nonce, plaintext) {
|
|
7394
|
+
const key = await import_node_crypto2.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
|
|
7395
|
+
const ct = await import_node_crypto2.webcrypto.subtle.encrypt(
|
|
7396
|
+
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
7397
|
+
key,
|
|
7398
|
+
plaintext
|
|
7399
|
+
);
|
|
7400
|
+
return new Uint8Array(ct);
|
|
7401
|
+
}
|
|
7402
|
+
async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
|
|
7403
|
+
const key = await import_node_crypto2.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
|
|
7404
|
+
try {
|
|
7405
|
+
const pt = await import_node_crypto2.webcrypto.subtle.decrypt(
|
|
7406
|
+
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
7407
|
+
key,
|
|
7408
|
+
ciphertextWithTag
|
|
7409
|
+
);
|
|
7410
|
+
return new Uint8Array(pt);
|
|
7411
|
+
} catch {
|
|
7412
|
+
throw new StatePassphraseInvalid();
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
function createEmptyState(identity) {
|
|
7416
|
+
return {
|
|
7417
|
+
version: STATE_VERSION,
|
|
7418
|
+
identity,
|
|
7419
|
+
handles: {},
|
|
7420
|
+
audit: [],
|
|
7421
|
+
pending: {}
|
|
7422
|
+
};
|
|
7423
|
+
}
|
|
7424
|
+
function validateEnvelope(json) {
|
|
7425
|
+
if (typeof json !== "object" || json === null) {
|
|
7426
|
+
throw new StateCorrupt("envelope is not an object");
|
|
7427
|
+
}
|
|
7428
|
+
const e = json;
|
|
7429
|
+
if (e.version !== STATE_VERSION) {
|
|
7430
|
+
throw new StateCorrupt(`unknown version: ${String(e.version)}`);
|
|
7431
|
+
}
|
|
7432
|
+
if (e.format !== FORMAT_TAG) {
|
|
7433
|
+
throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
|
|
7434
|
+
}
|
|
7435
|
+
const kdf = e.kdf;
|
|
7436
|
+
if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
|
|
7437
|
+
throw new StateCorrupt("kdf header missing or wrong algo");
|
|
7438
|
+
}
|
|
7439
|
+
const cipher = e.cipher;
|
|
7440
|
+
if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
|
|
7441
|
+
throw new StateCorrupt("cipher header missing or wrong algo");
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
function validatePlaintext(json) {
|
|
7445
|
+
if (typeof json !== "object" || json === null) {
|
|
7446
|
+
throw new StateCorrupt("plaintext is not an object");
|
|
7447
|
+
}
|
|
7448
|
+
const s = json;
|
|
7449
|
+
if (s.version !== STATE_VERSION) {
|
|
7450
|
+
throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
|
|
7451
|
+
}
|
|
7452
|
+
const id = s.identity;
|
|
7453
|
+
if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
|
|
7454
|
+
throw new StateCorrupt("identity missing or malformed");
|
|
7455
|
+
}
|
|
7456
|
+
if (typeof s.handles !== "object" || s.handles === null) {
|
|
7457
|
+
throw new StateCorrupt("handles is not an object");
|
|
7458
|
+
}
|
|
7459
|
+
if (!Array.isArray(s.audit)) {
|
|
7460
|
+
throw new StateCorrupt("audit is not an array");
|
|
7461
|
+
}
|
|
7462
|
+
if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
|
|
7463
|
+
throw new StateCorrupt("pending is not an object");
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
async function loadState(passphrase, statePath) {
|
|
7467
|
+
const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
|
|
7468
|
+
let envelope;
|
|
7469
|
+
try {
|
|
7470
|
+
envelope = JSON.parse(raw);
|
|
7471
|
+
} catch (err2) {
|
|
7472
|
+
throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
|
|
7473
|
+
}
|
|
7474
|
+
validateEnvelope(envelope);
|
|
7475
|
+
const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
|
|
7476
|
+
memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
|
|
7477
|
+
iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
|
|
7478
|
+
parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
|
|
7479
|
+
});
|
|
7480
|
+
const nonce = fromB642(envelope.cipher.nonce_b64);
|
|
7481
|
+
if (nonce.length !== AES_NONCE_BYTES) {
|
|
7482
|
+
throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
|
|
7483
|
+
}
|
|
7484
|
+
const ct = fromB642(envelope.cipher.ciphertext_b64);
|
|
7485
|
+
const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
|
|
7486
|
+
const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
|
|
7487
|
+
let plaintext;
|
|
7488
|
+
try {
|
|
7489
|
+
plaintext = JSON.parse(ptText);
|
|
7490
|
+
} catch (err2) {
|
|
7491
|
+
throw new StateCorrupt(`plaintext is not valid JSON: ${err2.message}`);
|
|
7492
|
+
}
|
|
7493
|
+
validatePlaintext(plaintext);
|
|
7494
|
+
if (plaintext.pending === void 0) {
|
|
7495
|
+
plaintext.pending = {};
|
|
7496
|
+
}
|
|
7497
|
+
return plaintext;
|
|
7498
|
+
}
|
|
7499
|
+
async function saveState(state, passphrase, statePath) {
|
|
7500
|
+
validatePlaintext(state);
|
|
7501
|
+
const salt = (0, import_node_crypto2.randomBytes)(ARGON2_SALT_BYTES);
|
|
7502
|
+
const nonce = (0, import_node_crypto2.randomBytes)(AES_NONCE_BYTES);
|
|
7503
|
+
const kek = await deriveKek(passphrase, toB642(salt), CURRENT_KDF_PARAMS);
|
|
7504
|
+
const ptBytes = new TextEncoder().encode(JSON.stringify(state));
|
|
7505
|
+
const ct = await aesGcmEncrypt(kek, nonce, ptBytes);
|
|
7506
|
+
const envelope = {
|
|
7507
|
+
version: STATE_VERSION,
|
|
7508
|
+
format: FORMAT_TAG,
|
|
7509
|
+
kdf: {
|
|
7510
|
+
algo: "argon2id",
|
|
7511
|
+
memory_kib: ARGON2_MEMORY_KIB,
|
|
7512
|
+
iterations: ARGON2_ITERATIONS,
|
|
7513
|
+
parallelism: ARGON2_PARALLELISM,
|
|
7514
|
+
salt_b64: toB642(salt)
|
|
7515
|
+
},
|
|
7516
|
+
cipher: {
|
|
7517
|
+
algo: "aes-256-gcm",
|
|
7518
|
+
nonce_b64: toB642(nonce),
|
|
7519
|
+
ciphertext_b64: toB642(ct),
|
|
7520
|
+
tag_included: true
|
|
7521
|
+
}
|
|
7522
|
+
};
|
|
7523
|
+
const dir = (0, import_node_path3.dirname)(statePath);
|
|
7524
|
+
await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
7525
|
+
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
7526
|
+
const fh = await import_node_fs2.promises.open(tmp, "w", 384);
|
|
7527
|
+
try {
|
|
7528
|
+
await fh.writeFile(JSON.stringify(envelope), "utf8");
|
|
7529
|
+
await fh.sync();
|
|
7530
|
+
} finally {
|
|
7531
|
+
await fh.close();
|
|
7532
|
+
}
|
|
7533
|
+
await import_node_fs2.promises.rename(tmp, statePath);
|
|
7534
|
+
try {
|
|
7535
|
+
const dh = await import_node_fs2.promises.open(dir, "r");
|
|
7536
|
+
try {
|
|
7537
|
+
await dh.sync();
|
|
7538
|
+
} finally {
|
|
7539
|
+
await dh.close();
|
|
7540
|
+
}
|
|
7541
|
+
} catch {
|
|
7542
|
+
}
|
|
7543
|
+
await import_node_fs2.promises.chmod(dir, 448).catch(() => {
|
|
7544
|
+
});
|
|
7545
|
+
}
|
|
7546
|
+
function updateState(passphrase, statePath, mutate) {
|
|
7547
|
+
return withStateLock((0, import_node_path3.dirname)(statePath), async () => {
|
|
7548
|
+
const view = await loadState(passphrase, statePath);
|
|
7549
|
+
if (await mutate(view) !== false) await saveState(view, passphrase, statePath);
|
|
7550
|
+
return view;
|
|
7551
|
+
});
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
// src/seed.ts
|
|
7555
|
+
var SEED_ENV_VAR = "SEAL_SEED";
|
|
7556
|
+
var EPHEMERAL_ENV_VAR = "SEAL_MCP_EPHEMERAL";
|
|
7557
|
+
var PASSPHRASE_ENV_VAR = "SEAL_MCP_PASSPHRASE";
|
|
7558
|
+
var SEED_FILE_NAME = "seed";
|
|
7559
|
+
var KEYCHAIN_SERVICE = "seal-mcp";
|
|
7560
|
+
var KEYCHAIN_ACCOUNT = "seed";
|
|
7561
|
+
var LEGACY_KEYCHAIN_ACCOUNT = "state";
|
|
7562
|
+
var SeedInvalidError = class extends Error {
|
|
7563
|
+
code = "seed_invalid";
|
|
7564
|
+
constructor(where) {
|
|
7565
|
+
super(
|
|
7566
|
+
`${where} is not an agent key: expected 43 base64url characters, as \`sealnet-mcp seed --force\` or \`seal keygen --export --force\` print them`
|
|
7567
|
+
);
|
|
7568
|
+
this.name = "SeedInvalidError";
|
|
7569
|
+
}
|
|
7570
|
+
};
|
|
7571
|
+
var IdentityConflictError = class extends Error {
|
|
7572
|
+
code = "identity_conflict";
|
|
7573
|
+
constructor(statePath) {
|
|
7574
|
+
super(
|
|
7575
|
+
`${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.`
|
|
7576
|
+
);
|
|
7577
|
+
this.name = "IdentityConflictError";
|
|
7578
|
+
}
|
|
7579
|
+
};
|
|
7580
|
+
function keyringEntry(keyring, platform, service, account) {
|
|
7581
|
+
return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
|
|
7582
|
+
}
|
|
7583
|
+
function readKeytarLinuxEntry(service, account) {
|
|
7584
|
+
return new Promise((resolve5) => {
|
|
7585
|
+
(0, import_node_child_process.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
|
|
7586
|
+
const value = err2 ? "" : stdout.replace(/\n$/, "");
|
|
7587
|
+
resolve5(value.length > 0 ? value : null);
|
|
7588
|
+
});
|
|
7589
|
+
});
|
|
7590
|
+
}
|
|
7591
|
+
function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
|
|
7592
|
+
const entry = (service, account) => keyringEntry(keyring, platform, service, account);
|
|
7593
|
+
const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
|
|
7594
|
+
return {
|
|
7595
|
+
async getPassword(service, account) {
|
|
7596
|
+
const secret = await entry(service, account).getSecret();
|
|
7597
|
+
if (secret !== void 0) return Buffer.from(secret).toString("utf8");
|
|
7598
|
+
const legacy = platform === "linux" ? await readLegacy(service, account) : null;
|
|
7599
|
+
if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
|
|
7600
|
+
return legacy;
|
|
7601
|
+
},
|
|
7602
|
+
setPassword: store,
|
|
7603
|
+
deletePassword: (service, account) => entry(service, account).deleteCredential()
|
|
7604
|
+
};
|
|
7605
|
+
}
|
|
7606
|
+
var keytarCache = void 0;
|
|
7607
|
+
async function loadKeytar() {
|
|
7608
|
+
if (keytarCache !== void 0) return keytarCache;
|
|
7609
|
+
try {
|
|
7610
|
+
const mod = await import("@napi-rs/keyring");
|
|
7611
|
+
const keyring = mod.default ?? mod;
|
|
7612
|
+
keyringEntry(keyring, process.platform, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
7613
|
+
keytarCache = keytarOverKeyring(keyring);
|
|
7614
|
+
} catch {
|
|
7615
|
+
keytarCache = null;
|
|
7616
|
+
}
|
|
7617
|
+
return keytarCache;
|
|
7618
|
+
}
|
|
7619
|
+
async function keychainGet(account) {
|
|
7620
|
+
const keytar = await loadKeytar();
|
|
7621
|
+
if (keytar === null) return null;
|
|
7622
|
+
try {
|
|
7623
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, account);
|
|
7624
|
+
return value !== null && value.length > 0 ? value : null;
|
|
7625
|
+
} catch {
|
|
7626
|
+
return null;
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
async function keychainDelete(account) {
|
|
7630
|
+
const keytar = await loadKeytar();
|
|
7631
|
+
if (keytar === null) return false;
|
|
7632
|
+
try {
|
|
7633
|
+
return await keytar.deletePassword(KEYCHAIN_SERVICE, account);
|
|
7634
|
+
} catch {
|
|
7635
|
+
return false;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
function parseSeed(text, where) {
|
|
7639
|
+
const seed = Buffer.from(text, "base64url");
|
|
7640
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(text) || seed.toString("base64url") !== text) throw new SeedInvalidError(where);
|
|
7641
|
+
return new Uint8Array(seed);
|
|
7642
|
+
}
|
|
7643
|
+
async function readSeed(stateDir, env = process.env) {
|
|
7644
|
+
const fromEnv = env[SEED_ENV_VAR]?.trim();
|
|
7645
|
+
if (fromEnv) return { seed: parseSeed(fromEnv, SEED_ENV_VAR), source: "env" };
|
|
7646
|
+
const stored = await keychainGet(KEYCHAIN_ACCOUNT);
|
|
7647
|
+
if (stored !== null) return { seed: parseSeed(stored, `keychain entry ${KEYCHAIN_SERVICE}/${KEYCHAIN_ACCOUNT}`), source: "keychain" };
|
|
7648
|
+
const file = (0, import_node_path4.join)(stateDir, SEED_FILE_NAME);
|
|
7649
|
+
let text;
|
|
7650
|
+
try {
|
|
7651
|
+
text = await import_node_fs3.promises.readFile(file, "utf8");
|
|
7652
|
+
} catch (err2) {
|
|
7653
|
+
if (err2.code === "ENOENT") return null;
|
|
7654
|
+
throw err2;
|
|
7655
|
+
}
|
|
7656
|
+
return { seed: parseSeed(text.trim(), file), source: "file" };
|
|
7657
|
+
}
|
|
7658
|
+
async function storeSeed(stateDir, seed) {
|
|
7659
|
+
const text = Buffer.from(seed).toString("base64url");
|
|
7660
|
+
const keytar = await loadKeytar();
|
|
7661
|
+
if (keytar !== null) {
|
|
7662
|
+
try {
|
|
7663
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, text);
|
|
7664
|
+
return "keychain";
|
|
7665
|
+
} catch {
|
|
7666
|
+
}
|
|
7667
|
+
}
|
|
7668
|
+
const fh = await import_node_fs3.promises.open((0, import_node_path4.join)(stateDir, SEED_FILE_NAME), "wx", 384);
|
|
7669
|
+
try {
|
|
7670
|
+
await fh.writeFile(`${text}
|
|
7671
|
+
`, "utf8");
|
|
7672
|
+
await fh.sync();
|
|
7673
|
+
} finally {
|
|
7674
|
+
await fh.close();
|
|
7675
|
+
}
|
|
7676
|
+
return "file";
|
|
7677
|
+
}
|
|
7678
|
+
function statePassphrase(seed) {
|
|
7679
|
+
return Buffer.from((0, import_node_crypto3.hkdfSync)("sha256", seed, "seal-mcp:state:v1", "passphrase", 32)).toString("base64url");
|
|
7680
|
+
}
|
|
7681
|
+
async function legacyPassphrase(env) {
|
|
7682
|
+
const stored = await keychainGet(LEGACY_KEYCHAIN_ACCOUNT);
|
|
7683
|
+
if (stored !== null) return { passphrase: stored, source: "keychain" };
|
|
7684
|
+
const fromEnv = env[PASSPHRASE_ENV_VAR];
|
|
7685
|
+
return fromEnv ? { passphrase: fromEnv, source: "env" } : null;
|
|
7686
|
+
}
|
|
7687
|
+
async function tryOpen(passphrase, statePath) {
|
|
7688
|
+
try {
|
|
7689
|
+
return await loadState(passphrase, statePath);
|
|
7690
|
+
} catch (err2) {
|
|
7691
|
+
if (err2 instanceof StatePassphraseInvalid) return null;
|
|
7692
|
+
throw err2;
|
|
7693
|
+
}
|
|
7694
|
+
}
|
|
7695
|
+
async function requireIdentity(view, seed, statePath) {
|
|
7696
|
+
const { secret } = await loadIdentity(view.identity);
|
|
7697
|
+
if (!Buffer.from(secret).equals(Buffer.from(seed))) throw new IdentityConflictError(statePath);
|
|
7698
|
+
}
|
|
7699
|
+
function openAgentState(path, env = process.env) {
|
|
7700
|
+
return withStateLock(path.dir, async () => {
|
|
7701
|
+
let root = await readSeed(path.dir, env);
|
|
7702
|
+
if (!await import_node_fs3.promises.stat(path.statePath).then(() => true, () => false)) {
|
|
7703
|
+
if (root === null) {
|
|
7704
|
+
const { secret } = await generateIdentity();
|
|
7705
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
7706
|
+
}
|
|
7707
|
+
const passphrase2 = statePassphrase(root.seed);
|
|
7708
|
+
const identity = await identityFromSeed(root.seed);
|
|
7709
|
+
await saveState(createEmptyState(identityToFields(identity)), passphrase2, path.statePath);
|
|
7710
|
+
return { root, passphrase: passphrase2, created: true, moved: null };
|
|
7711
|
+
}
|
|
7712
|
+
if (root !== null) {
|
|
7713
|
+
const view2 = await tryOpen(statePassphrase(root.seed), path.statePath);
|
|
7714
|
+
if (view2 !== null) {
|
|
7715
|
+
await requireIdentity(view2, root.seed, path.statePath);
|
|
7716
|
+
return { root, passphrase: statePassphrase(root.seed), created: false, moved: null };
|
|
7717
|
+
}
|
|
7718
|
+
}
|
|
7719
|
+
const legacy = await legacyPassphrase(env);
|
|
7720
|
+
const view = legacy === null ? null : await tryOpen(legacy.passphrase, path.statePath);
|
|
7721
|
+
if (legacy === null || view === null) throw new IdentityConflictError(path.statePath);
|
|
7722
|
+
if (root !== null) await requireIdentity(view, root.seed, path.statePath);
|
|
7723
|
+
else {
|
|
7724
|
+
const { secret } = await loadIdentity(view.identity);
|
|
7725
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
7726
|
+
}
|
|
7727
|
+
const passphrase = statePassphrase(root.seed);
|
|
7728
|
+
await saveState(view, passphrase, path.statePath);
|
|
7729
|
+
await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
7730
|
+
return { root, passphrase, created: false, moved: legacy.source };
|
|
7731
|
+
});
|
|
7732
|
+
}
|
|
7733
|
+
async function deleteRootSecret(stateDir) {
|
|
7734
|
+
let seedFile = true;
|
|
7735
|
+
await import_node_fs3.promises.unlink((0, import_node_path4.join)(stateDir, SEED_FILE_NAME)).catch((err2) => {
|
|
7736
|
+
if (err2.code !== "ENOENT") throw err2;
|
|
7737
|
+
seedFile = false;
|
|
7738
|
+
});
|
|
7739
|
+
const seed = await keychainDelete(KEYCHAIN_ACCOUNT);
|
|
7740
|
+
const legacy = await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
7741
|
+
return { seedFile, keychain: seed || legacy };
|
|
7742
|
+
}
|
|
7743
|
+
async function probeKeychain() {
|
|
7744
|
+
const keytar = await loadKeytar();
|
|
7745
|
+
if (keytar === null) return { available: false, hasSeed: false };
|
|
7746
|
+
try {
|
|
7747
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
7748
|
+
return { available: true, hasSeed: value !== null && value.length > 0 };
|
|
7749
|
+
} catch {
|
|
7750
|
+
return { available: false, hasSeed: false };
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
|
|
7754
|
+
// src/server.ts
|
|
7755
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7756
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7757
|
+
|
|
7758
|
+
// ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
7759
|
+
var external_exports = {};
|
|
7760
|
+
__export(external_exports, {
|
|
7761
|
+
BRAND: () => BRAND,
|
|
7762
|
+
DIRTY: () => DIRTY,
|
|
7763
|
+
EMPTY_PATH: () => EMPTY_PATH,
|
|
7764
|
+
INVALID: () => INVALID,
|
|
7765
|
+
NEVER: () => NEVER,
|
|
7766
|
+
OK: () => OK,
|
|
7767
|
+
ParseStatus: () => ParseStatus,
|
|
7768
|
+
Schema: () => ZodType,
|
|
7769
|
+
ZodAny: () => ZodAny,
|
|
7770
|
+
ZodArray: () => ZodArray,
|
|
7771
|
+
ZodBigInt: () => ZodBigInt,
|
|
7772
|
+
ZodBoolean: () => ZodBoolean,
|
|
7773
|
+
ZodBranded: () => ZodBranded,
|
|
7774
|
+
ZodCatch: () => ZodCatch,
|
|
7775
|
+
ZodDate: () => ZodDate,
|
|
7776
|
+
ZodDefault: () => ZodDefault,
|
|
7777
|
+
ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
|
|
7778
|
+
ZodEffects: () => ZodEffects,
|
|
7779
|
+
ZodEnum: () => ZodEnum,
|
|
7780
|
+
ZodError: () => ZodError,
|
|
7781
|
+
ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
|
|
7782
|
+
ZodFunction: () => ZodFunction,
|
|
7783
|
+
ZodIntersection: () => ZodIntersection,
|
|
7784
|
+
ZodIssueCode: () => ZodIssueCode,
|
|
7785
|
+
ZodLazy: () => ZodLazy,
|
|
7786
|
+
ZodLiteral: () => ZodLiteral,
|
|
7787
|
+
ZodMap: () => ZodMap,
|
|
7788
|
+
ZodNaN: () => ZodNaN,
|
|
7789
|
+
ZodNativeEnum: () => ZodNativeEnum,
|
|
7790
|
+
ZodNever: () => ZodNever,
|
|
7791
|
+
ZodNull: () => ZodNull,
|
|
7792
|
+
ZodNullable: () => ZodNullable,
|
|
7793
|
+
ZodNumber: () => ZodNumber,
|
|
7794
|
+
ZodObject: () => ZodObject,
|
|
7795
|
+
ZodOptional: () => ZodOptional,
|
|
7796
|
+
ZodParsedType: () => ZodParsedType,
|
|
7797
|
+
ZodPipeline: () => ZodPipeline,
|
|
7798
|
+
ZodPromise: () => ZodPromise,
|
|
7799
|
+
ZodReadonly: () => ZodReadonly,
|
|
7800
|
+
ZodRecord: () => ZodRecord,
|
|
7801
|
+
ZodSchema: () => ZodType,
|
|
7802
|
+
ZodSet: () => ZodSet,
|
|
7803
|
+
ZodString: () => ZodString,
|
|
7804
|
+
ZodSymbol: () => ZodSymbol,
|
|
7805
|
+
ZodTransformer: () => ZodEffects,
|
|
7806
|
+
ZodTuple: () => ZodTuple,
|
|
7807
|
+
ZodType: () => ZodType,
|
|
7808
|
+
ZodUndefined: () => ZodUndefined,
|
|
7809
|
+
ZodUnion: () => ZodUnion,
|
|
7810
|
+
ZodUnknown: () => ZodUnknown,
|
|
7811
|
+
ZodVoid: () => ZodVoid,
|
|
7812
|
+
addIssueToContext: () => addIssueToContext,
|
|
7813
|
+
any: () => anyType,
|
|
7814
|
+
array: () => arrayType,
|
|
7815
|
+
bigint: () => bigIntType,
|
|
7816
|
+
boolean: () => booleanType,
|
|
7817
|
+
coerce: () => coerce,
|
|
7818
|
+
custom: () => custom,
|
|
7819
|
+
date: () => dateType,
|
|
7820
|
+
datetimeRegex: () => datetimeRegex,
|
|
7821
|
+
defaultErrorMap: () => en_default,
|
|
7822
|
+
discriminatedUnion: () => discriminatedUnionType,
|
|
7823
|
+
effect: () => effectsType,
|
|
7824
|
+
enum: () => enumType,
|
|
7825
|
+
function: () => functionType,
|
|
7826
|
+
getErrorMap: () => getErrorMap,
|
|
7827
|
+
getParsedType: () => getParsedType,
|
|
7828
|
+
instanceof: () => instanceOfType,
|
|
7829
|
+
intersection: () => intersectionType,
|
|
7830
|
+
isAborted: () => isAborted,
|
|
7831
|
+
isAsync: () => isAsync,
|
|
7832
|
+
isDirty: () => isDirty,
|
|
7833
|
+
isValid: () => isValid,
|
|
7834
|
+
late: () => late,
|
|
7835
|
+
lazy: () => lazyType,
|
|
7836
|
+
literal: () => literalType,
|
|
7837
|
+
makeIssue: () => makeIssue,
|
|
7838
|
+
map: () => mapType,
|
|
7839
|
+
nan: () => nanType,
|
|
7840
|
+
nativeEnum: () => nativeEnumType,
|
|
7841
|
+
never: () => neverType,
|
|
7842
|
+
null: () => nullType,
|
|
7843
|
+
nullable: () => nullableType,
|
|
7844
|
+
number: () => numberType,
|
|
7845
|
+
object: () => objectType,
|
|
7406
7846
|
objectUtil: () => objectUtil,
|
|
7407
7847
|
oboolean: () => oboolean,
|
|
7408
7848
|
onumber: () => onumber,
|
|
@@ -11371,7 +11811,7 @@ var seal_mcp_v1_default = {
|
|
|
11371
11811
|
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
11812
|
},
|
|
11373
11813
|
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). Never read or print a SEAL handoff file.",
|
|
11814
|
+
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). Files sent to your key arrive in seal_inbox; open them with seal_open item. Never read or print a SEAL handoff file.",
|
|
11375
11815
|
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
11816
|
},
|
|
11377
11817
|
tools: {
|
|
@@ -11421,7 +11861,7 @@ var seal_mcp_v1_default = {
|
|
|
11421
11861
|
required: ["handle", "receipt", "handoff_channel"],
|
|
11422
11862
|
properties: {
|
|
11423
11863
|
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 `
|
|
11864
|
+
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
11865
|
handoff_channel: { type: "string", enum: ["clipboard", "file"] },
|
|
11426
11866
|
handoff_file: { type: "string", description: "ONLY when handoff_channel='file'. Absolute path to a 0o600 file containing the share link and a newline." },
|
|
11427
11867
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
@@ -11439,7 +11879,8 @@ var seal_mcp_v1_default = {
|
|
|
11439
11879
|
share_url: { type: "string", format: "uri", description: "`targeted_x25519_v1` link: the wrapped key is in the fragment, useless without the recipient's X25519 secret; there is no separate password." },
|
|
11440
11880
|
expires_at: { type: "string", format: "date-time" },
|
|
11441
11881
|
max_reads: { type: "integer", const: 1 },
|
|
11442
|
-
mode: { type: "string", const: "forward" }
|
|
11882
|
+
mode: { type: "string", const: "forward" },
|
|
11883
|
+
delivered: { type: "boolean", description: "The link also lies in the recipient's inbox (SPEC-ADDRESS F2): the server keeps its wrapped key in the link's row, and the holder of the key finds it with seal_inbox without being handed the URL." }
|
|
11443
11884
|
},
|
|
11444
11885
|
policy: {
|
|
11445
11886
|
max_expire_minutes: 30,
|
|
@@ -11507,9 +11948,11 @@ var seal_mcp_v1_default = {
|
|
|
11507
11948
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
11508
11949
|
annotations_note: "Set on the tool, not per mode: `inline` and `file` consume an open, so the tool as a whole is not read-only.",
|
|
11509
11950
|
input: {
|
|
11510
|
-
|
|
11951
|
+
one_of: ["url", "item"],
|
|
11952
|
+
one_of_note: "Exactly one of `url` and `item`; `server.ts` takes both as optional and the handler refuses anything else with `url_invalid`.",
|
|
11511
11953
|
properties: {
|
|
11512
11954
|
url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
|
|
11955
|
+
item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
|
|
11513
11956
|
mode: { type: "string", enum: ["metadata", "inline", "file"], default: "metadata", description: "`metadata` reads nothing; `file` saves to disk; `inline` returns small text." },
|
|
11514
11957
|
dir: { type: "string", description: "mode=file: directory to save into; default ~/Downloads, created if missing. An existing file is never overwritten: the new one becomes `name (1).ext`." }
|
|
11515
11958
|
}
|
|
@@ -11554,16 +11997,50 @@ var seal_mcp_v1_default = {
|
|
|
11554
11997
|
max_reads: { type: "integer", minimum: 1 },
|
|
11555
11998
|
expires_at: { type: "string", format: "date-time" }
|
|
11556
11999
|
},
|
|
11557
|
-
does_not_consume_counters: true
|
|
11558
|
-
},
|
|
11559
|
-
output_inline_allowed_mimes: ["application/json", "text/csv", "text/plain"],
|
|
11560
|
-
output_inline_max_bytes: 102400,
|
|
11561
|
-
output_inline_refusals: {
|
|
11562
|
-
"secrets.json::mime": "inline_blocked.secret",
|
|
11563
|
-
"text/html": "inline_blocked.injection_risk",
|
|
11564
|
-
"application/pdf": "inline_blocked.injection_risk",
|
|
11565
|
-
"application/xhtml+xml": "inline_blocked.injection_risk",
|
|
11566
|
-
_binary_default: "inline_blocked.binary"
|
|
12000
|
+
does_not_consume_counters: true
|
|
12001
|
+
},
|
|
12002
|
+
output_inline_allowed_mimes: ["application/json", "text/csv", "text/plain"],
|
|
12003
|
+
output_inline_max_bytes: 102400,
|
|
12004
|
+
output_inline_refusals: {
|
|
12005
|
+
"secrets.json::mime": "inline_blocked.secret",
|
|
12006
|
+
"text/html": "inline_blocked.injection_risk",
|
|
12007
|
+
"application/pdf": "inline_blocked.injection_risk",
|
|
12008
|
+
"application/xhtml+xml": "inline_blocked.injection_risk",
|
|
12009
|
+
_binary_default: "inline_blocked.binary"
|
|
12010
|
+
}
|
|
12011
|
+
},
|
|
12012
|
+
seal_inbox: {
|
|
12013
|
+
title: "Check the inbox",
|
|
12014
|
+
description: "Seals other agents or people sent to your key (your address). Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
|
|
12015
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12016
|
+
input: {
|
|
12017
|
+
properties: {
|
|
12018
|
+
wait: { type: "integer", minimum: 0, maximum: 300, default: 0, description: "Seconds to wait for an item this server has not seen yet; 0 returns the inbox now." }
|
|
12019
|
+
}
|
|
12020
|
+
},
|
|
12021
|
+
flow: "`GET /intake?to=<this server's address>` under the proof of the key (seal_address.v1.json proof), cached until 30 s before it expires. The first call in a process returns the list at once; later ones long-poll by 25 s with `seen` = the version of the last answer until an id this process has not seen arrives or `wait` runs out. Handles, rows and the version live in process memory.",
|
|
12022
|
+
output: {
|
|
12023
|
+
type: "object",
|
|
12024
|
+
required: ["address_emoji", "items"],
|
|
12025
|
+
properties: {
|
|
12026
|
+
address_emoji: { type: "string", description: "Three emoji of this server's address (`fingerprint_emoji.json`)." },
|
|
12027
|
+
items: {
|
|
12028
|
+
type: "array",
|
|
12029
|
+
items: {
|
|
12030
|
+
type: "object",
|
|
12031
|
+
required: ["handle", "from", "label", "received_at", "expires_at"],
|
|
12032
|
+
additionalProperties: false,
|
|
12033
|
+
properties: {
|
|
12034
|
+
handle: { type: "string", pattern: "^in_", description: "`in_` + the inbox item id; pass as seal_open `item`." },
|
|
12035
|
+
from: { type: ["string", "null"], description: "Three emoji of the sender's address when the server verified it, else null." },
|
|
12036
|
+
label: { type: ["string", "null"], description: "What the sender called itself (an agent's name, `sealnet`), in the clear: never content." },
|
|
12037
|
+
received_at: { type: "string", format: "date-time" },
|
|
12038
|
+
expires_at: { type: "string", format: "date-time" }
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
},
|
|
12043
|
+
MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
|
|
11567
12044
|
}
|
|
11568
12045
|
},
|
|
11569
12046
|
seal_list: {
|
|
@@ -11757,7 +12234,7 @@ var seal_mcp_v1_default = {
|
|
|
11757
12234
|
type: "object",
|
|
11758
12235
|
required: ["x25519_secret_b64", "x25519_public_b64"],
|
|
11759
12236
|
properties: {
|
|
11760
|
-
x25519_secret_b64: { type: "string", description: "32 raw bytes,
|
|
12237
|
+
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." },
|
|
11761
12238
|
x25519_public_b64: { type: "string", description: "32 raw bytes, derived from secret. Safe to publish; advertised so other parties can target this MCP." }
|
|
11762
12239
|
}
|
|
11763
12240
|
},
|
|
@@ -11778,7 +12255,7 @@ var seal_mcp_v1_default = {
|
|
|
11778
12255
|
},
|
|
11779
12256
|
pending: {
|
|
11780
12257
|
type: "object",
|
|
11781
|
-
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.",
|
|
12258
|
+
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.",
|
|
11782
12259
|
additionalProperties: {
|
|
11783
12260
|
type: "object",
|
|
11784
12261
|
required: ["tool", "mode", "started_at", "owner_token_b64", "expires_at", "step"],
|
|
@@ -11791,6 +12268,12 @@ var seal_mcp_v1_default = {
|
|
|
11791
12268
|
step: { type: "string", enum: ["preparing", "awaiting_payment", "uploaded", "delivering", "compensating"] },
|
|
11792
12269
|
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." },
|
|
11793
12270
|
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)." },
|
|
12271
|
+
owner: {
|
|
12272
|
+
type: "object",
|
|
12273
|
+
required: ["pid", "hostname"],
|
|
12274
|
+
properties: { pid: { type: "integer" }, hostname: { type: "string" } },
|
|
12275
|
+
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."
|
|
12276
|
+
},
|
|
11794
12277
|
compensation: {
|
|
11795
12278
|
type: "object",
|
|
11796
12279
|
required: ["action", "outcome", "error", "at"],
|
|
@@ -11822,27 +12305,20 @@ var seal_mcp_v1_default = {
|
|
|
11822
12305
|
}
|
|
11823
12306
|
}
|
|
11824
12307
|
},
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
default: true
|
|
11832
|
-
},
|
|
11833
|
-
{
|
|
11834
|
-
rank: 2,
|
|
11835
|
-
method: "env_var",
|
|
11836
|
-
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."
|
|
11837
|
-
},
|
|
11838
|
-
{
|
|
11839
|
-
rank: 3,
|
|
11840
|
-
method: "REFUSED",
|
|
11841
|
-
implementation: "Cleartext passphrase file on disk is explicitly refused even if user passes `--passphrase-file \u2026`. Error message points at options 1 and 2."
|
|
11842
|
-
}
|
|
12308
|
+
root_secret: {
|
|
12309
|
+
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.",
|
|
12310
|
+
ordered_sources: [
|
|
12311
|
+
{ rank: 1, source: "env", implementation: "$SEAL_SEED. How a container or a cloud session gets the same address as at home." },
|
|
12312
|
+
{ 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." },
|
|
12313
|
+
{ 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." }
|
|
11843
12314
|
],
|
|
11844
|
-
|
|
11845
|
-
|
|
12315
|
+
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.",
|
|
12316
|
+
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.",
|
|
12317
|
+
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.",
|
|
12318
|
+
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.",
|
|
12319
|
+
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.",
|
|
12320
|
+
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`.",
|
|
12321
|
+
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."
|
|
11846
12322
|
},
|
|
11847
12323
|
handoff_delivery: {
|
|
11848
12324
|
primary_channel: "system_clipboard",
|
|
@@ -11890,6 +12366,10 @@ var seal_mcp_v1_default = {
|
|
|
11890
12366
|
share_blob_corrupt: "Share blob decoded but a file_key is not valid base64url or has the wrong length (32 bytes expected). Backend bug or share-blob tampering.",
|
|
11891
12367
|
share_blob_failed: "`encryptShareBlob` threw during seal_share mint (Web Crypto failure or input shape bug). Caller-facing in seal_share path only.",
|
|
11892
12368
|
share_link_failed: "POST /seal/{id}/share returned an error (network, 5xx, owner_token mismatch).",
|
|
12369
|
+
inbox_proof_failed: "seal_inbox / seal_open item / seal_share forward: the server refused the proof of this server's key (`intake.proof_required`) even after one fresh challenge.",
|
|
12370
|
+
inbox_full: "seal_share forward: the recipient's inbox holds `limits.json::INBOX_MAX_ITEMS` live items from other senders (`intake.inbox_full`). The seal is revoked; retry later.",
|
|
12371
|
+
inbox_item_gone: "seal_open item: the handle is not in this process's inbox and not in a fresh read of it \u2014 the seal or link was revoked or expired, or the item was removed.",
|
|
12372
|
+
inbox_failed: "seal_inbox / seal_open item: reading or cleaning the inbox failed for a reason other than the proof (network, 5xx, rate limit); the message has the status. Safe to retry.",
|
|
11893
12373
|
targeted_fragment_invalid: "seal_open of `x25519:` URL: fragment is malformed base64url, wrong packed length, or AEAD-failed for a non-secret reason. Distinct from `targeted_decrypt_failed` which is the wrong-recipient case.",
|
|
11894
12374
|
targeted_decrypt_failed: "seal_open of `x25519:` URL: AEAD verification failed under THIS device's identity. The URL was not addressed to this MCP's pubkey (or was tampered, or seal_id from path-segment was substituted). Recipient should ask the sender to re-mint with `--to <our pubkey>`.",
|
|
11895
12375
|
auth_required: "Backend returned 401/403 because the seal has OTP / NDA / login gating; interactive auth is not supported in MCP. Open the URL in a browser.",
|
|
@@ -11918,11 +12398,12 @@ var seal_mcp_v1_default = {
|
|
|
11918
12398
|
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.",
|
|
11919
12399
|
pro_failed: "seal_pro_*: SEAL Pro answered with another error; the message has the status.",
|
|
11920
12400
|
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.",
|
|
12401
|
+
state_busy: "Another sealnet-mcp process held the state lock for more than 5 s. Nothing was changed by this call; safe to retry.",
|
|
11921
12402
|
runtime_unsupported: "Node runtime is missing a global the bundle assumes (e.g. `globalThis.File` or `globalThis.Blob`). Upgrade to Node \u226520.",
|
|
11922
12403
|
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.",
|
|
11923
|
-
|
|
12404
|
+
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`.",
|
|
11924
12405
|
"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.",
|
|
11925
|
-
state_dir_unsafe: "
|
|
12406
|
+
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.",
|
|
11926
12407
|
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."
|
|
11927
12408
|
},
|
|
11928
12409
|
current_format_version: 1,
|
|
@@ -11949,7 +12430,7 @@ function safeFilename(name) {
|
|
|
11949
12430
|
return cleaned.length > 0 ? cleaned : "seal-file";
|
|
11950
12431
|
}
|
|
11951
12432
|
async function receiveToDisk(opened, dir, name) {
|
|
11952
|
-
const
|
|
12433
|
+
const fs4 = await import(
|
|
11953
12434
|
/* webpackIgnore: true */
|
|
11954
12435
|
/* @vite-ignore */
|
|
11955
12436
|
"fs/promises"
|
|
@@ -11959,10 +12440,10 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11959
12440
|
/* @vite-ignore */
|
|
11960
12441
|
"path"
|
|
11961
12442
|
);
|
|
11962
|
-
await
|
|
12443
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
11963
12444
|
const suffix = Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11964
12445
|
const tmp = path.join(dir, `.seal-partial-${suffix}`);
|
|
11965
|
-
const fh = await
|
|
12446
|
+
const fh = await fs4.open(tmp, "wx", 384);
|
|
11966
12447
|
let placed = false;
|
|
11967
12448
|
try {
|
|
11968
12449
|
await opened.stream.pipeTo(
|
|
@@ -11980,12 +12461,12 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11980
12461
|
for (let i = 0; i < 1e3; i++) {
|
|
11981
12462
|
const candidate = path.join(dir, i === 0 ? name : `${stem} (${i})${ext}`);
|
|
11982
12463
|
try {
|
|
11983
|
-
await
|
|
12464
|
+
await fs4.link(tmp, candidate);
|
|
11984
12465
|
} catch (err2) {
|
|
11985
12466
|
if (err2.code === "EEXIST") continue;
|
|
11986
12467
|
throw err2;
|
|
11987
12468
|
}
|
|
11988
|
-
await
|
|
12469
|
+
await fs4.unlink(tmp);
|
|
11989
12470
|
placed = true;
|
|
11990
12471
|
return { path: candidate, sizeBytes, sha256: sha2562 };
|
|
11991
12472
|
}
|
|
@@ -11993,7 +12474,7 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11993
12474
|
} finally {
|
|
11994
12475
|
if (!placed) {
|
|
11995
12476
|
await fh.close().catch(() => void 0);
|
|
11996
|
-
await
|
|
12477
|
+
await fs4.unlink(tmp).catch(() => void 0);
|
|
11997
12478
|
}
|
|
11998
12479
|
}
|
|
11999
12480
|
}
|
|
@@ -12379,6 +12860,10 @@ async function recoverPendingOperations(view, client, opts = {}) {
|
|
|
12379
12860
|
log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
|
|
12380
12861
|
continue;
|
|
12381
12862
|
}
|
|
12863
|
+
if (op.owner !== void 0 && op.owner.pid !== process.pid && holderMayRun(op.owner)) {
|
|
12864
|
+
log4(`[seal-mcp] recovery: left ${handle} alone \u2014 pid ${op.owner.pid} on ${op.owner.hostname} may still run it`);
|
|
12865
|
+
continue;
|
|
12866
|
+
}
|
|
12382
12867
|
if (op.seal_id === void 0) {
|
|
12383
12868
|
delete view.pending[handle];
|
|
12384
12869
|
report.dropped.push(handle);
|
|
@@ -12399,274 +12884,52 @@ async function recoverPendingOperations(view, client, opts = {}) {
|
|
|
12399
12884
|
op.step = "compensating";
|
|
12400
12885
|
op.compensation = {
|
|
12401
12886
|
action: "revoke",
|
|
12402
|
-
outcome: "failed",
|
|
12403
|
-
error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
|
|
12404
|
-
at: nowIso(opts.now)
|
|
12405
|
-
};
|
|
12406
|
-
report.stillFailing.push(handle);
|
|
12407
|
-
changed = true;
|
|
12408
|
-
log4(
|
|
12409
|
-
`[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`
|
|
12410
|
-
);
|
|
12411
|
-
continue;
|
|
12412
|
-
}
|
|
12413
|
-
try {
|
|
12414
|
-
await client.revoke(op.seal_id, op.owner_token_b64);
|
|
12415
|
-
delete view.pending[handle];
|
|
12416
|
-
report.revoked.push(handle);
|
|
12417
|
-
changed = true;
|
|
12418
|
-
log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
|
|
12419
|
-
} catch (err2) {
|
|
12420
|
-
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
12421
|
-
op.step = "compensating";
|
|
12422
|
-
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
|
|
12423
|
-
report.stillFailing.push(handle);
|
|
12424
|
-
changed = true;
|
|
12425
|
-
log4(
|
|
12426
|
-
`[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
|
|
12427
|
-
);
|
|
12428
|
-
}
|
|
12429
|
-
}
|
|
12430
|
-
return { report, changed };
|
|
12431
|
-
}
|
|
12432
|
-
function adoptAsHandle(view, handle, op, now) {
|
|
12433
|
-
const sealId = op.seal_id;
|
|
12434
|
-
if (view.handles[handle] === void 0) {
|
|
12435
|
-
view.handles[handle] = {
|
|
12436
|
-
seal_id: sealId,
|
|
12437
|
-
owner_token_b64: op.owner_token_b64,
|
|
12438
|
-
created_at: op.started_at,
|
|
12439
|
-
mode: op.mode,
|
|
12440
|
-
expires_at: op.expires_at,
|
|
12441
|
-
// Propagate provenance so a later seal_revoke on the adopted
|
|
12442
|
-
// handle still refuses to target the wrong backend.
|
|
12443
|
-
backend_url: op.backend_url
|
|
12444
|
-
};
|
|
12445
|
-
view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
|
|
12446
|
-
}
|
|
12447
|
-
delete view.pending[handle];
|
|
12448
|
-
}
|
|
12449
|
-
|
|
12450
|
-
// src/state.ts
|
|
12451
|
-
var import_node_crypto3 = require("crypto");
|
|
12452
|
-
var import_node_fs2 = require("fs");
|
|
12453
|
-
var import_node_path3 = require("path");
|
|
12454
|
-
var import_argon2 = __toESM(require("argon2"), 1);
|
|
12455
|
-
var FORMAT_TAG = "SEAL_MCP_STATE v1";
|
|
12456
|
-
var STATE_VERSION = 1;
|
|
12457
|
-
var ARGON2_MEMORY_KIB = 65536;
|
|
12458
|
-
var ARGON2_ITERATIONS = 3;
|
|
12459
|
-
var ARGON2_PARALLELISM = 4;
|
|
12460
|
-
var ARGON2_SALT_BYTES = 16;
|
|
12461
|
-
var KDF_BOUNDS = {
|
|
12462
|
-
memory_kib: { min: 8192, max: 1048576 },
|
|
12463
|
-
// 8 MiB .. 1 GiB
|
|
12464
|
-
iterations: { min: 1, max: 16 },
|
|
12465
|
-
parallelism: { min: 1, max: 16 }
|
|
12466
|
-
};
|
|
12467
|
-
var AES_KEY_BYTES = 32;
|
|
12468
|
-
var AES_NONCE_BYTES = 12;
|
|
12469
|
-
var StatePassphraseInvalid = class extends Error {
|
|
12470
|
-
constructor() {
|
|
12471
|
-
super("state.passphrase_invalid: AES-256-GCM tag check failed");
|
|
12472
|
-
this.name = "StatePassphraseInvalid";
|
|
12473
|
-
}
|
|
12474
|
-
};
|
|
12475
|
-
var StateCorrupt = class extends Error {
|
|
12476
|
-
constructor(reason) {
|
|
12477
|
-
super(`state.corrupt: ${reason}`);
|
|
12478
|
-
this.name = "StateCorrupt";
|
|
12479
|
-
}
|
|
12480
|
-
};
|
|
12481
|
-
function toB642(bytes) {
|
|
12482
|
-
return Buffer.from(bytes).toString("base64");
|
|
12483
|
-
}
|
|
12484
|
-
function fromB642(s) {
|
|
12485
|
-
return new Uint8Array(Buffer.from(s, "base64"));
|
|
12486
|
-
}
|
|
12487
|
-
var CURRENT_KDF_PARAMS = {
|
|
12488
|
-
memory_kib: ARGON2_MEMORY_KIB,
|
|
12489
|
-
iterations: ARGON2_ITERATIONS,
|
|
12490
|
-
parallelism: ARGON2_PARALLELISM
|
|
12491
|
-
};
|
|
12492
|
-
function boundedKdfParam(name, value) {
|
|
12493
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
12494
|
-
throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
|
|
12495
|
-
}
|
|
12496
|
-
const { min, max } = KDF_BOUNDS[name];
|
|
12497
|
-
if (value < min || value > max) {
|
|
12498
|
-
throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
|
|
12499
|
-
}
|
|
12500
|
-
return value;
|
|
12501
|
-
}
|
|
12502
|
-
async function deriveKek(passphrase, saltB64, params) {
|
|
12503
|
-
const salt = fromB642(saltB64);
|
|
12504
|
-
if (salt.length !== ARGON2_SALT_BYTES) {
|
|
12505
|
-
throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
|
|
12506
|
-
}
|
|
12507
|
-
const kek = await import_argon2.default.hash(passphrase, {
|
|
12508
|
-
type: import_argon2.default.argon2id,
|
|
12509
|
-
raw: true,
|
|
12510
|
-
salt: Buffer.from(salt),
|
|
12511
|
-
memoryCost: params.memory_kib,
|
|
12512
|
-
timeCost: params.iterations,
|
|
12513
|
-
parallelism: params.parallelism,
|
|
12514
|
-
hashLength: AES_KEY_BYTES
|
|
12515
|
-
});
|
|
12516
|
-
return new Uint8Array(kek);
|
|
12517
|
-
}
|
|
12518
|
-
async function aesGcmEncrypt(kek, nonce, plaintext) {
|
|
12519
|
-
const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
|
|
12520
|
-
const ct = await import_node_crypto3.webcrypto.subtle.encrypt(
|
|
12521
|
-
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
12522
|
-
key,
|
|
12523
|
-
plaintext
|
|
12524
|
-
);
|
|
12525
|
-
return new Uint8Array(ct);
|
|
12526
|
-
}
|
|
12527
|
-
async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
|
|
12528
|
-
const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
|
|
12529
|
-
try {
|
|
12530
|
-
const pt = await import_node_crypto3.webcrypto.subtle.decrypt(
|
|
12531
|
-
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
12532
|
-
key,
|
|
12533
|
-
ciphertextWithTag
|
|
12534
|
-
);
|
|
12535
|
-
return new Uint8Array(pt);
|
|
12536
|
-
} catch {
|
|
12537
|
-
throw new StatePassphraseInvalid();
|
|
12538
|
-
}
|
|
12539
|
-
}
|
|
12540
|
-
function createEmptyState(identity) {
|
|
12541
|
-
return {
|
|
12542
|
-
version: STATE_VERSION,
|
|
12543
|
-
identity,
|
|
12544
|
-
handles: {},
|
|
12545
|
-
audit: [],
|
|
12546
|
-
pending: {}
|
|
12547
|
-
};
|
|
12548
|
-
}
|
|
12549
|
-
function validateEnvelope(json) {
|
|
12550
|
-
if (typeof json !== "object" || json === null) {
|
|
12551
|
-
throw new StateCorrupt("envelope is not an object");
|
|
12552
|
-
}
|
|
12553
|
-
const e = json;
|
|
12554
|
-
if (e.version !== STATE_VERSION) {
|
|
12555
|
-
throw new StateCorrupt(`unknown version: ${String(e.version)}`);
|
|
12556
|
-
}
|
|
12557
|
-
if (e.format !== FORMAT_TAG) {
|
|
12558
|
-
throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
|
|
12559
|
-
}
|
|
12560
|
-
const kdf = e.kdf;
|
|
12561
|
-
if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
|
|
12562
|
-
throw new StateCorrupt("kdf header missing or wrong algo");
|
|
12563
|
-
}
|
|
12564
|
-
const cipher = e.cipher;
|
|
12565
|
-
if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
|
|
12566
|
-
throw new StateCorrupt("cipher header missing or wrong algo");
|
|
12567
|
-
}
|
|
12568
|
-
}
|
|
12569
|
-
function validatePlaintext(json) {
|
|
12570
|
-
if (typeof json !== "object" || json === null) {
|
|
12571
|
-
throw new StateCorrupt("plaintext is not an object");
|
|
12572
|
-
}
|
|
12573
|
-
const s = json;
|
|
12574
|
-
if (s.version !== STATE_VERSION) {
|
|
12575
|
-
throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
|
|
12576
|
-
}
|
|
12577
|
-
const id = s.identity;
|
|
12578
|
-
if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
|
|
12579
|
-
throw new StateCorrupt("identity missing or malformed");
|
|
12580
|
-
}
|
|
12581
|
-
if (typeof s.handles !== "object" || s.handles === null) {
|
|
12582
|
-
throw new StateCorrupt("handles is not an object");
|
|
12583
|
-
}
|
|
12584
|
-
if (!Array.isArray(s.audit)) {
|
|
12585
|
-
throw new StateCorrupt("audit is not an array");
|
|
12586
|
-
}
|
|
12587
|
-
if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
|
|
12588
|
-
throw new StateCorrupt("pending is not an object");
|
|
12589
|
-
}
|
|
12590
|
-
}
|
|
12591
|
-
async function loadState(passphrase, statePath) {
|
|
12592
|
-
const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
|
|
12593
|
-
let envelope;
|
|
12594
|
-
try {
|
|
12595
|
-
envelope = JSON.parse(raw);
|
|
12596
|
-
} catch (err2) {
|
|
12597
|
-
throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
|
|
12598
|
-
}
|
|
12599
|
-
validateEnvelope(envelope);
|
|
12600
|
-
const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
|
|
12601
|
-
memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
|
|
12602
|
-
iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
|
|
12603
|
-
parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
|
|
12604
|
-
});
|
|
12605
|
-
const nonce = fromB642(envelope.cipher.nonce_b64);
|
|
12606
|
-
if (nonce.length !== AES_NONCE_BYTES) {
|
|
12607
|
-
throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
|
|
12608
|
-
}
|
|
12609
|
-
const ct = fromB642(envelope.cipher.ciphertext_b64);
|
|
12610
|
-
const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
|
|
12611
|
-
const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
|
|
12612
|
-
let plaintext;
|
|
12613
|
-
try {
|
|
12614
|
-
plaintext = JSON.parse(ptText);
|
|
12615
|
-
} catch (err2) {
|
|
12616
|
-
throw new StateCorrupt(`plaintext is not valid JSON: ${err2.message}`);
|
|
12617
|
-
}
|
|
12618
|
-
validatePlaintext(plaintext);
|
|
12619
|
-
if (plaintext.pending === void 0) {
|
|
12620
|
-
plaintext.pending = {};
|
|
12621
|
-
}
|
|
12622
|
-
return plaintext;
|
|
12623
|
-
}
|
|
12624
|
-
async function saveState(state, passphrase, statePath) {
|
|
12625
|
-
validatePlaintext(state);
|
|
12626
|
-
const salt = (0, import_node_crypto3.randomBytes)(ARGON2_SALT_BYTES);
|
|
12627
|
-
const nonce = (0, import_node_crypto3.randomBytes)(AES_NONCE_BYTES);
|
|
12628
|
-
const kek = await deriveKek(passphrase, toB642(salt), CURRENT_KDF_PARAMS);
|
|
12629
|
-
const ptBytes = new TextEncoder().encode(JSON.stringify(state));
|
|
12630
|
-
const ct = await aesGcmEncrypt(kek, nonce, ptBytes);
|
|
12631
|
-
const envelope = {
|
|
12632
|
-
version: STATE_VERSION,
|
|
12633
|
-
format: FORMAT_TAG,
|
|
12634
|
-
kdf: {
|
|
12635
|
-
algo: "argon2id",
|
|
12636
|
-
memory_kib: ARGON2_MEMORY_KIB,
|
|
12637
|
-
iterations: ARGON2_ITERATIONS,
|
|
12638
|
-
parallelism: ARGON2_PARALLELISM,
|
|
12639
|
-
salt_b64: toB642(salt)
|
|
12640
|
-
},
|
|
12641
|
-
cipher: {
|
|
12642
|
-
algo: "aes-256-gcm",
|
|
12643
|
-
nonce_b64: toB642(nonce),
|
|
12644
|
-
ciphertext_b64: toB642(ct),
|
|
12645
|
-
tag_included: true
|
|
12887
|
+
outcome: "failed",
|
|
12888
|
+
error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
|
|
12889
|
+
at: nowIso(opts.now)
|
|
12890
|
+
};
|
|
12891
|
+
report.stillFailing.push(handle);
|
|
12892
|
+
changed = true;
|
|
12893
|
+
log4(
|
|
12894
|
+
`[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`
|
|
12895
|
+
);
|
|
12896
|
+
continue;
|
|
12646
12897
|
}
|
|
12647
|
-
};
|
|
12648
|
-
const dir = (0, import_node_path3.dirname)(statePath);
|
|
12649
|
-
await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
12650
|
-
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
12651
|
-
const fh = await import_node_fs2.promises.open(tmp, "w", 384);
|
|
12652
|
-
try {
|
|
12653
|
-
await fh.writeFile(JSON.stringify(envelope), "utf8");
|
|
12654
|
-
await fh.sync();
|
|
12655
|
-
} finally {
|
|
12656
|
-
await fh.close();
|
|
12657
|
-
}
|
|
12658
|
-
await import_node_fs2.promises.rename(tmp, statePath);
|
|
12659
|
-
try {
|
|
12660
|
-
const dh = await import_node_fs2.promises.open(dir, "r");
|
|
12661
12898
|
try {
|
|
12662
|
-
await
|
|
12663
|
-
|
|
12664
|
-
|
|
12899
|
+
await client.revoke(op.seal_id, op.owner_token_b64);
|
|
12900
|
+
delete view.pending[handle];
|
|
12901
|
+
report.revoked.push(handle);
|
|
12902
|
+
changed = true;
|
|
12903
|
+
log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
|
|
12904
|
+
} catch (err2) {
|
|
12905
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
12906
|
+
op.step = "compensating";
|
|
12907
|
+
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
|
|
12908
|
+
report.stillFailing.push(handle);
|
|
12909
|
+
changed = true;
|
|
12910
|
+
log4(
|
|
12911
|
+
`[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
|
|
12912
|
+
);
|
|
12665
12913
|
}
|
|
12666
|
-
} catch {
|
|
12667
12914
|
}
|
|
12668
|
-
|
|
12669
|
-
|
|
12915
|
+
return { report, changed };
|
|
12916
|
+
}
|
|
12917
|
+
function adoptAsHandle(view, handle, op, now) {
|
|
12918
|
+
const sealId = op.seal_id;
|
|
12919
|
+
if (view.handles[handle] === void 0) {
|
|
12920
|
+
view.handles[handle] = {
|
|
12921
|
+
seal_id: sealId,
|
|
12922
|
+
owner_token_b64: op.owner_token_b64,
|
|
12923
|
+
created_at: op.started_at,
|
|
12924
|
+
mode: op.mode,
|
|
12925
|
+
expires_at: op.expires_at,
|
|
12926
|
+
// Propagate provenance so a later seal_revoke on the adopted
|
|
12927
|
+
// handle still refuses to target the wrong backend.
|
|
12928
|
+
backend_url: op.backend_url
|
|
12929
|
+
};
|
|
12930
|
+
view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
|
|
12931
|
+
}
|
|
12932
|
+
delete view.pending[handle];
|
|
12670
12933
|
}
|
|
12671
12934
|
|
|
12672
12935
|
// src/tools/context.ts
|
|
@@ -12685,19 +12948,18 @@ var HANDLE_REGEX = /^h_[A-Za-z0-9_-]{12,}$/;
|
|
|
12685
12948
|
function isValidHandle(handle) {
|
|
12686
12949
|
return typeof handle === "string" && HANDLE_REGEX.test(handle);
|
|
12687
12950
|
}
|
|
12951
|
+
function auditEntry(ctx, tool, handle, mode) {
|
|
12952
|
+
return { ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(), tool, handle, mode };
|
|
12953
|
+
}
|
|
12688
12954
|
function appendAudit(ctx, tool, handle, mode) {
|
|
12689
|
-
ctx
|
|
12690
|
-
|
|
12691
|
-
tool,
|
|
12692
|
-
handle,
|
|
12693
|
-
mode
|
|
12694
|
-
});
|
|
12955
|
+
const entry = auditEntry(ctx, tool, handle, mode);
|
|
12956
|
+
return ctx.update((state) => void state.audit.push(entry));
|
|
12695
12957
|
}
|
|
12696
|
-
function insertHandle(
|
|
12697
|
-
if (
|
|
12958
|
+
function insertHandle(state, handle, entry) {
|
|
12959
|
+
if (state.handles[handle]) {
|
|
12698
12960
|
throw new Error(`insertHandle: collision on ${handle}`);
|
|
12699
12961
|
}
|
|
12700
|
-
|
|
12962
|
+
state.handles[handle] = entry;
|
|
12701
12963
|
}
|
|
12702
12964
|
function lookupHandle(ctx, handle) {
|
|
12703
12965
|
const entry = ctx.state.handles[handle];
|
|
@@ -12717,9 +12979,76 @@ var ToolError = class extends Error {
|
|
|
12717
12979
|
}
|
|
12718
12980
|
};
|
|
12719
12981
|
|
|
12982
|
+
// src/tools/inbox.ts
|
|
12983
|
+
var HANDLE_PREFIX2 = "in_";
|
|
12984
|
+
function inboxError(err2) {
|
|
12985
|
+
if (!(err2 instanceof BackendError)) return err2;
|
|
12986
|
+
if (err2.appCode === "intake.proof_required") {
|
|
12987
|
+
return new ToolError("inbox_proof_failed", `the server refused the proof of this server's key: ${err2.message}`, err2);
|
|
12988
|
+
}
|
|
12989
|
+
return new ToolError("inbox_failed", `the inbox failed (${err2.status}): ${err2.message}`, err2);
|
|
12990
|
+
}
|
|
12991
|
+
function memoryOf(ctx) {
|
|
12992
|
+
if (!ctx.inbox) throw new ToolError("internal_inconsistency", "this server keeps no inbox memory");
|
|
12993
|
+
return ctx.inbox;
|
|
12994
|
+
}
|
|
12995
|
+
async function sealInbox(ctx, input = {}) {
|
|
12996
|
+
const memory = memoryOf(ctx);
|
|
12997
|
+
const deadline = Date.now() + (memory.version === null ? 0 : (input.wait ?? 0) * 1e3);
|
|
12998
|
+
let seen = memory.version ?? void 0;
|
|
12999
|
+
let inbox;
|
|
13000
|
+
for (; ; ) {
|
|
13001
|
+
const left = Math.ceil((deadline - Date.now()) / 1e3);
|
|
13002
|
+
try {
|
|
13003
|
+
inbox = await readInbox(ctx.client, ctx.identity.secret, left > 0 ? { waitSeconds: left, seen } : {});
|
|
13004
|
+
} catch (err2) {
|
|
13005
|
+
throw inboxError(err2);
|
|
13006
|
+
}
|
|
13007
|
+
const arrived = inbox.items.some((item) => !memory.items.has(HANDLE_PREFIX2 + item.id));
|
|
13008
|
+
if (left <= 0 || arrived || Date.now() >= deadline) break;
|
|
13009
|
+
seen = inbox.version;
|
|
13010
|
+
}
|
|
13011
|
+
memory.items.clear();
|
|
13012
|
+
for (const item of inbox.items) memory.items.set(HANDLE_PREFIX2 + item.id, item);
|
|
13013
|
+
memory.version = inbox.version;
|
|
13014
|
+
return {
|
|
13015
|
+
addressEmoji: keyEmoji(ctx.identity.public),
|
|
13016
|
+
items: inbox.items.map((item) => ({
|
|
13017
|
+
handle: HANDLE_PREFIX2 + item.id,
|
|
13018
|
+
from: item.sender === null ? null : keyEmoji(base64urlToBytes(item.sender)),
|
|
13019
|
+
label: item.label,
|
|
13020
|
+
receivedAt: item.at,
|
|
13021
|
+
expiresAt: item.expires_at
|
|
13022
|
+
}))
|
|
13023
|
+
};
|
|
13024
|
+
}
|
|
13025
|
+
async function inboxItem(ctx, handle) {
|
|
13026
|
+
const memory = memoryOf(ctx);
|
|
13027
|
+
if (!memory.items.has(handle)) await sealInbox(ctx, {});
|
|
13028
|
+
const item = memory.items.get(handle);
|
|
13029
|
+
if (!item) {
|
|
13030
|
+
throw new ToolError(
|
|
13031
|
+
"inbox_item_gone",
|
|
13032
|
+
`${handle} is not in the inbox: the seal or link was revoked or expired, or the item was removed`
|
|
13033
|
+
);
|
|
13034
|
+
}
|
|
13035
|
+
return item;
|
|
13036
|
+
}
|
|
13037
|
+
async function forgetInboxItem(ctx, handle) {
|
|
13038
|
+
const item = memoryOf(ctx).items.get(handle);
|
|
13039
|
+
if (!item) return;
|
|
13040
|
+
try {
|
|
13041
|
+
await removeFromInbox(ctx.client, ctx.identity.secret, item.id);
|
|
13042
|
+
} catch (err2) {
|
|
13043
|
+
throw inboxError(err2);
|
|
13044
|
+
}
|
|
13045
|
+
memoryOf(ctx).items.delete(handle);
|
|
13046
|
+
}
|
|
13047
|
+
|
|
12720
13048
|
// src/tools/list.ts
|
|
12721
13049
|
var SINGLE_FILE_COUNT = 1;
|
|
12722
13050
|
async function sealList(ctx, input = {}) {
|
|
13051
|
+
await appendAudit(ctx, "seal_list", null, null);
|
|
12723
13052
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
12724
13053
|
const entries = [];
|
|
12725
13054
|
for (const [handle, entry] of Object.entries(ctx.state.handles)) {
|
|
@@ -12736,15 +13065,13 @@ async function sealList(ctx, input = {}) {
|
|
|
12736
13065
|
});
|
|
12737
13066
|
}
|
|
12738
13067
|
entries.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
12739
|
-
appendAudit(ctx, "seal_list", null, null);
|
|
12740
|
-
await ctx.persist();
|
|
12741
13068
|
return { entries };
|
|
12742
13069
|
}
|
|
12743
13070
|
|
|
12744
13071
|
// src/tools/open.ts
|
|
12745
13072
|
var import_promises3 = require("fs/promises");
|
|
12746
13073
|
var import_node_os3 = require("os");
|
|
12747
|
-
var
|
|
13074
|
+
var import_node_path5 = require("path");
|
|
12748
13075
|
var INLINE_ALLOWED_MIMES = ["application/json", "text/csv", "text/plain"];
|
|
12749
13076
|
var INLINE_INJECTION_RISK_MIMES = /* @__PURE__ */ new Set([
|
|
12750
13077
|
"text/html",
|
|
@@ -12809,11 +13136,28 @@ async function download(ctx, sealId, file) {
|
|
|
12809
13136
|
return resp.body;
|
|
12810
13137
|
}
|
|
12811
13138
|
async function sealOpen(ctx, input) {
|
|
13139
|
+
if (input.url === void 0 === (input.item === void 0)) {
|
|
13140
|
+
throw new ToolError("url_invalid", "give exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
|
|
13141
|
+
}
|
|
13142
|
+
const recipient = { recipientSecret: ctx.identity.secret };
|
|
13143
|
+
if (input.url !== void 0) return openSeal(ctx, input, () => openShareLink(ctx.client, input.url, recipient));
|
|
13144
|
+
const handle = input.item;
|
|
13145
|
+
const item = await inboxItem(ctx, handle);
|
|
13146
|
+
try {
|
|
13147
|
+
return await openSeal(ctx, input, () => openShareLink(ctx.client, inboxItemUrl(resolvePublicHost(), item), recipient));
|
|
13148
|
+
} catch (err2) {
|
|
13149
|
+
if (err2 instanceof ToolError && (err2.code === "seal_revoked" || err2.code === "share_link_revoked")) {
|
|
13150
|
+
await forgetInboxItem(ctx, handle).catch(() => void 0);
|
|
13151
|
+
}
|
|
13152
|
+
throw err2;
|
|
13153
|
+
}
|
|
13154
|
+
}
|
|
13155
|
+
async function openSeal(ctx, input, read) {
|
|
12812
13156
|
const mode = input.mode ?? "metadata";
|
|
12813
13157
|
const handle = generateHandle();
|
|
12814
13158
|
let seal;
|
|
12815
13159
|
try {
|
|
12816
|
-
seal = await
|
|
13160
|
+
seal = await read();
|
|
12817
13161
|
} catch (err2) {
|
|
12818
13162
|
throw asToolError(err2);
|
|
12819
13163
|
}
|
|
@@ -12822,13 +13166,11 @@ async function sealOpen(ctx, input) {
|
|
|
12822
13166
|
if (mode === "inline") {
|
|
12823
13167
|
const refusal = classifyInlineRefusal(first.mime);
|
|
12824
13168
|
if (refusal !== null) {
|
|
12825
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12826
|
-
await ctx.persist();
|
|
13169
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12827
13170
|
return { handle, refusal, mime: first.mime, note: refusalNote(refusal, first.mime) };
|
|
12828
13171
|
}
|
|
12829
13172
|
if (first.size > INLINE_CIPHERTEXT_MAX_BYTES) {
|
|
12830
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12831
|
-
await ctx.persist();
|
|
13173
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12832
13174
|
return {
|
|
12833
13175
|
handle,
|
|
12834
13176
|
refusal: "inline_blocked.too_large",
|
|
@@ -12864,12 +13206,10 @@ async function sealOpen(ctx, input) {
|
|
|
12864
13206
|
err2
|
|
12865
13207
|
);
|
|
12866
13208
|
}
|
|
12867
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12868
|
-
await ctx.persist();
|
|
13209
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12869
13210
|
return { handle, mime: bareMime(first.mime), sizeBytes: plaintext.byteLength, encoding: "utf-8", content };
|
|
12870
13211
|
}
|
|
12871
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12872
|
-
await ctx.persist();
|
|
13212
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12873
13213
|
return {
|
|
12874
13214
|
handle,
|
|
12875
13215
|
filename: first.name,
|
|
@@ -12891,7 +13231,7 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12891
13231
|
`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`
|
|
12892
13232
|
);
|
|
12893
13233
|
}
|
|
12894
|
-
const dir = (0,
|
|
13234
|
+
const dir = (0, import_node_path5.resolve)(input.dir ?? (0, import_node_path5.join)((0, import_node_os3.homedir)(), "Downloads"));
|
|
12895
13235
|
try {
|
|
12896
13236
|
await (0, import_promises3.mkdir)(dir, { recursive: true });
|
|
12897
13237
|
} catch (err2) {
|
|
@@ -12915,15 +13255,14 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12915
13255
|
throw streamError(err2, file.id);
|
|
12916
13256
|
}
|
|
12917
13257
|
}
|
|
12918
|
-
appendAudit(ctx, "seal_open", handle, "file");
|
|
12919
|
-
await ctx.persist();
|
|
13258
|
+
await appendAudit(ctx, "seal_open", handle, "file");
|
|
12920
13259
|
return { handle, files: out };
|
|
12921
13260
|
}
|
|
12922
13261
|
|
|
12923
13262
|
// src/tools/pro.ts
|
|
12924
13263
|
var import_promises4 = require("fs/promises");
|
|
12925
13264
|
var import_node_os4 = require("os");
|
|
12926
|
-
var
|
|
13265
|
+
var import_node_path6 = require("path");
|
|
12927
13266
|
var SECRET_CODES = {
|
|
12928
13267
|
not_found: "pro_secret_not_found",
|
|
12929
13268
|
not_granted: "pro_secret_not_granted",
|
|
@@ -12948,9 +13287,9 @@ async function asTool(fn, codes = SECRET_CODES) {
|
|
|
12948
13287
|
}
|
|
12949
13288
|
async function sealProSecretUse(source, input) {
|
|
12950
13289
|
const got = await asTool(() => source.getSecret(input.secret, input.version));
|
|
12951
|
-
const dir = await (0, import_promises4.mkdtemp)((0,
|
|
13290
|
+
const dir = await (0, import_promises4.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "seal-mcp-pro-"));
|
|
12952
13291
|
const name = (got.path?.split("/").pop() || got.id).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
12953
|
-
const path = (0,
|
|
13292
|
+
const path = (0, import_node_path6.join)(dir, name);
|
|
12954
13293
|
try {
|
|
12955
13294
|
await (0, import_promises4.writeFile)(path, got.value, { mode: 384, flag: "wx" });
|
|
12956
13295
|
return { path, sizeBytes: got.value.length, secretId: got.id, version: got.version };
|
|
@@ -12964,7 +13303,7 @@ async function sealProSecretRequest(source, input) {
|
|
|
12964
13303
|
return asTool(() => source.waitForAccess(asked));
|
|
12965
13304
|
}
|
|
12966
13305
|
async function sealProFileGet(source, input) {
|
|
12967
|
-
const dir = (0,
|
|
13306
|
+
const dir = (0, import_node_path6.resolve)(input.dir ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Downloads"));
|
|
12968
13307
|
const ids = input.file ? [input.file] : (await asTool(() => source.listFiles(input.seal), FILE_CODES)).map((f) => f.id);
|
|
12969
13308
|
const files = [];
|
|
12970
13309
|
for (const id of ids) {
|
|
@@ -12976,16 +13315,16 @@ async function sealProFileGet(source, input) {
|
|
|
12976
13315
|
|
|
12977
13316
|
// src/tools/request.ts
|
|
12978
13317
|
var import_node_child_process2 = require("child_process");
|
|
12979
|
-
var
|
|
13318
|
+
var import_node_fs4 = require("fs");
|
|
12980
13319
|
var import_promises5 = require("fs/promises");
|
|
12981
13320
|
var import_node_os5 = require("os");
|
|
12982
|
-
var
|
|
13321
|
+
var import_node_path7 = require("path");
|
|
12983
13322
|
function findSealCli() {
|
|
12984
13323
|
const names = process.platform === "win32" ? ["seal.exe", "seal.cmd"] : ["seal"];
|
|
12985
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
13324
|
+
for (const dir of (process.env.PATH ?? "").split(import_node_path7.delimiter)) {
|
|
12986
13325
|
for (const name of names) {
|
|
12987
|
-
const candidate = (0,
|
|
12988
|
-
if (dir && (0,
|
|
13326
|
+
const candidate = (0, import_node_path7.join)(dir, name);
|
|
13327
|
+
if (dir && (0, import_node_fs4.existsSync)(candidate)) return candidate;
|
|
12989
13328
|
}
|
|
12990
13329
|
}
|
|
12991
13330
|
return null;
|
|
@@ -13019,8 +13358,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
13019
13358
|
if (input.whereUrl) cliArgs.push("--where", input.whereUrl);
|
|
13020
13359
|
const local = await (deps.runCli ?? runSealCli)(cliArgs);
|
|
13021
13360
|
if (local && "files" in local) {
|
|
13022
|
-
appendAudit(ctx, "seal_request", handle, "local");
|
|
13023
|
-
await ctx.persist();
|
|
13361
|
+
await appendAudit(ctx, "seal_request", handle, "local");
|
|
13024
13362
|
return {
|
|
13025
13363
|
handle,
|
|
13026
13364
|
source: "local",
|
|
@@ -13060,8 +13398,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
13060
13398
|
}
|
|
13061
13399
|
return receiveThroughLink(ctx, handle, intake.id, deps, input.onProgress);
|
|
13062
13400
|
}
|
|
13063
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
13064
|
-
await ctx.persist();
|
|
13401
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
13065
13402
|
return { handle, url, emoji, expiresAt: intake.expires_at };
|
|
13066
13403
|
}
|
|
13067
13404
|
async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
@@ -13078,12 +13415,11 @@ async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
|
13078
13415
|
);
|
|
13079
13416
|
}
|
|
13080
13417
|
await deps.elicitationDone?.(intakeId).catch(() => void 0);
|
|
13081
|
-
const dir = await (0, import_promises5.mkdtemp)((0,
|
|
13418
|
+
const dir = await (0, import_promises5.mkdtemp)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "seal-mcp-request-"));
|
|
13082
13419
|
const opened = await sealOpen(ctx, { url: `${deps.publicHost}${sharePath}`, mode: "file", dir, onProgress });
|
|
13083
13420
|
if (!("files" in opened)) throw new ToolError("internal_inconsistency", "seal_open mode=file returned no files");
|
|
13084
13421
|
ctx.requests?.delete(handle);
|
|
13085
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
13086
|
-
await ctx.persist();
|
|
13422
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
13087
13423
|
return {
|
|
13088
13424
|
handle,
|
|
13089
13425
|
source: "link",
|
|
@@ -13096,6 +13432,7 @@ async function sealRevoke(ctx, input) {
|
|
|
13096
13432
|
if (!isValidHandle(input.handle)) {
|
|
13097
13433
|
throw new ToolError("handle_invalid", `handle does not match contract pattern: ${input.handle}`);
|
|
13098
13434
|
}
|
|
13435
|
+
await ctx.update(() => false);
|
|
13099
13436
|
const entry = lookupHandle(ctx, input.handle);
|
|
13100
13437
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13101
13438
|
if (entry.revoked_at) {
|
|
@@ -13130,16 +13467,18 @@ async function sealRevoke(ctx, input) {
|
|
|
13130
13467
|
err2
|
|
13131
13468
|
);
|
|
13132
13469
|
}
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13470
|
+
await ctx.update((state) => {
|
|
13471
|
+
state.handles[input.handle] = { ...entry, revoked_at: now };
|
|
13472
|
+
state.audit.push(auditEntry(ctx, "seal_revoke", input.handle, entry.mode));
|
|
13473
|
+
});
|
|
13136
13474
|
return { revoked: true, revokedAt: now };
|
|
13137
13475
|
}
|
|
13138
13476
|
|
|
13139
13477
|
// src/tools/share.ts
|
|
13140
|
-
var
|
|
13478
|
+
var import_node_fs6 = require("fs");
|
|
13141
13479
|
var import_promises9 = require("fs/promises");
|
|
13142
|
-
var
|
|
13480
|
+
var import_node_os8 = require("os");
|
|
13481
|
+
var import_node_path10 = require("path");
|
|
13143
13482
|
|
|
13144
13483
|
// ../../contracts/limits.json
|
|
13145
13484
|
var limits_default2 = {
|
|
@@ -13166,14 +13505,16 @@ var limits_default2 = {
|
|
|
13166
13505
|
INTAKE_TTL_DEFAULT_SECONDS: 900,
|
|
13167
13506
|
INTAKE_TTL_MAX_SECONDS: 86400,
|
|
13168
13507
|
INTAKE_WAIT_MAX_SECONDS: 25,
|
|
13169
|
-
PAYMENT_WAIT_MAX_SECONDS: 82800
|
|
13508
|
+
PAYMENT_WAIT_MAX_SECONDS: 82800,
|
|
13509
|
+
ADDRESS_PROOF_TTL_SECONDS: 300,
|
|
13510
|
+
INBOX_MAX_ITEMS: 200
|
|
13170
13511
|
}
|
|
13171
13512
|
};
|
|
13172
13513
|
|
|
13173
13514
|
// src/handoff.ts
|
|
13174
13515
|
var import_promises6 = require("fs/promises");
|
|
13175
13516
|
var import_node_os6 = require("os");
|
|
13176
|
-
var
|
|
13517
|
+
var import_node_path8 = require("path");
|
|
13177
13518
|
var ONE_HOUR_MS = 36e5;
|
|
13178
13519
|
var HandoffBothChannelsFailedError = class extends Error {
|
|
13179
13520
|
code = "handoff_both_channels_failed";
|
|
@@ -13189,10 +13530,10 @@ var HandoffBothChannelsFailedError = class extends Error {
|
|
|
13189
13530
|
function handoffDir(deps) {
|
|
13190
13531
|
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
13191
13532
|
const base = deps.tmpdirOverride ?? (xdg && xdg.length > 0 ? xdg : (0, import_node_os6.tmpdir)());
|
|
13192
|
-
return (0,
|
|
13533
|
+
return (0, import_node_path8.join)(base, "seal-mcp");
|
|
13193
13534
|
}
|
|
13194
13535
|
function handoffPath(deps, handle) {
|
|
13195
|
-
return (0,
|
|
13536
|
+
return (0, import_node_path8.join)(handoffDir(deps), `handoff-${handle}.txt`);
|
|
13196
13537
|
}
|
|
13197
13538
|
async function writeClipboard(text) {
|
|
13198
13539
|
const mod = await import("clipboardy");
|
|
@@ -13259,10 +13600,10 @@ async function performHandoff(payload, deps = {}) {
|
|
|
13259
13600
|
}
|
|
13260
13601
|
|
|
13261
13602
|
// src/zip.ts
|
|
13262
|
-
var
|
|
13603
|
+
var import_node_fs5 = require("fs");
|
|
13263
13604
|
var import_promises7 = require("fs/promises");
|
|
13264
13605
|
var import_node_os7 = require("os");
|
|
13265
|
-
var
|
|
13606
|
+
var import_node_path9 = require("path");
|
|
13266
13607
|
var import_node_stream = require("stream");
|
|
13267
13608
|
var import_promises8 = require("stream/promises");
|
|
13268
13609
|
var ZIP32_MAX_BYTES = 4294967295;
|
|
@@ -13270,16 +13611,16 @@ async function collectFiles(dir) {
|
|
|
13270
13611
|
const found = [];
|
|
13271
13612
|
const walk = async (current) => {
|
|
13272
13613
|
for (const dirent of await (0, import_promises7.readdir)(current, { withFileTypes: true })) {
|
|
13273
|
-
const abs = (0,
|
|
13614
|
+
const abs = (0, import_node_path9.join)(current, dirent.name);
|
|
13274
13615
|
if (dirent.isDirectory()) await walk(abs);
|
|
13275
|
-
else if (dirent.isFile()) found.push({ rel: (0,
|
|
13616
|
+
else if (dirent.isFile()) found.push({ rel: (0, import_node_path9.relative)(dir, abs).split(import_node_path9.sep).join("/"), abs });
|
|
13276
13617
|
}
|
|
13277
13618
|
};
|
|
13278
13619
|
await walk(dir);
|
|
13279
13620
|
found.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
13280
13621
|
return Promise.all(
|
|
13281
13622
|
found.map(async ({ rel, abs }) => {
|
|
13282
|
-
const file = new File([await (0,
|
|
13623
|
+
const file = new File([await (0, import_node_fs5.openAsBlob)(abs)], (0, import_node_path9.basename)(abs));
|
|
13283
13624
|
return Object.defineProperty(file, "webkitRelativePath", { value: rel });
|
|
13284
13625
|
})
|
|
13285
13626
|
);
|
|
@@ -13289,19 +13630,19 @@ async function planFolderArchive(dir) {
|
|
|
13289
13630
|
if (files.length === 0) {
|
|
13290
13631
|
throw new Error(`directory contains no regular files: ${dir}`);
|
|
13291
13632
|
}
|
|
13292
|
-
const folderName = (0,
|
|
13633
|
+
const folderName = (0, import_node_path9.basename)((0, import_node_path9.resolve)(dir)) || "folder";
|
|
13293
13634
|
const size = estimateZipSize({ files, folderName });
|
|
13294
13635
|
if (size > ZIP32_MAX_BYTES) {
|
|
13295
13636
|
throw new Error(`the folder's archive would be ${size} bytes, over the ZIP limit of ${ZIP32_MAX_BYTES}`);
|
|
13296
13637
|
}
|
|
13297
13638
|
const write = async () => {
|
|
13298
|
-
const tempDir = await (0, import_promises7.mkdtemp)((0,
|
|
13639
|
+
const tempDir = await (0, import_promises7.mkdtemp)((0, import_node_path9.join)((0, import_node_os7.tmpdir)(), "seal-mcp-zip-"));
|
|
13299
13640
|
const cleanup = () => (0, import_promises7.rm)(tempDir, { recursive: true, force: true });
|
|
13300
|
-
const path = (0,
|
|
13641
|
+
const path = (0, import_node_path9.join)(tempDir, `${folderName}.zip`);
|
|
13301
13642
|
try {
|
|
13302
13643
|
await (0, import_promises8.pipeline)(
|
|
13303
13644
|
import_node_stream.Readable.fromWeb(createZipStream2({ files, folderName })),
|
|
13304
|
-
(0,
|
|
13645
|
+
(0, import_node_fs5.createWriteStream)(path, { mode: 384 })
|
|
13305
13646
|
);
|
|
13306
13647
|
const written = (await (0, import_promises7.stat)(path)).size;
|
|
13307
13648
|
if (written !== size) {
|
|
@@ -13393,31 +13734,46 @@ var FORWARD_MAX_READS_FORCED = 1;
|
|
|
13393
13734
|
function nowIso2(ctx) {
|
|
13394
13735
|
return (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13395
13736
|
}
|
|
13396
|
-
|
|
13737
|
+
function journal(ctx, handle, op) {
|
|
13738
|
+
return ctx.update((state) => {
|
|
13739
|
+
state.pending[handle] = op;
|
|
13740
|
+
});
|
|
13741
|
+
}
|
|
13742
|
+
function dropJournal(ctx, handle) {
|
|
13743
|
+
return ctx.update((state) => {
|
|
13744
|
+
delete state.pending[handle];
|
|
13745
|
+
});
|
|
13746
|
+
}
|
|
13747
|
+
function complete(ctx, handle, entry) {
|
|
13748
|
+
return ctx.update((state) => {
|
|
13749
|
+
insertHandle(state, handle, entry);
|
|
13750
|
+
state.audit.push(auditEntry(ctx, "seal_share", handle, entry.mode));
|
|
13751
|
+
delete state.pending[handle];
|
|
13752
|
+
});
|
|
13753
|
+
}
|
|
13754
|
+
async function bestEffort(write) {
|
|
13397
13755
|
try {
|
|
13398
|
-
await
|
|
13756
|
+
await write;
|
|
13399
13757
|
} catch (err2) {
|
|
13400
13758
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13401
|
-
process.stderr.write(`[seal-mcp] warn: journal
|
|
13759
|
+
process.stderr.write(`[seal-mcp] warn: journal write failed: ${msg}
|
|
13402
13760
|
`);
|
|
13403
13761
|
}
|
|
13404
13762
|
}
|
|
13405
|
-
async function compensateRevoke(ctx, handle) {
|
|
13406
|
-
const
|
|
13407
|
-
|
|
13408
|
-
if (!op || sealId === void 0) {
|
|
13763
|
+
async function compensateRevoke(ctx, handle, op) {
|
|
13764
|
+
const sealId = op.seal_id;
|
|
13765
|
+
if (sealId === void 0) {
|
|
13409
13766
|
return "no live seal to revoke";
|
|
13410
13767
|
}
|
|
13411
13768
|
try {
|
|
13412
13769
|
await ctx.client.revoke(sealId, op.owner_token_b64);
|
|
13413
|
-
|
|
13414
|
-
await persistBestEffort(ctx);
|
|
13770
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13415
13771
|
return "seal auto-revoked";
|
|
13416
13772
|
} catch (err2) {
|
|
13417
13773
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13418
13774
|
op.step = "compensating";
|
|
13419
13775
|
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso2(ctx) };
|
|
13420
|
-
await
|
|
13776
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13421
13777
|
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`;
|
|
13422
13778
|
}
|
|
13423
13779
|
}
|
|
@@ -13442,7 +13798,7 @@ async function openSource(path, mime, requestedTier) {
|
|
|
13442
13798
|
} catch (err2) {
|
|
13443
13799
|
throw failed("unable to open", err2);
|
|
13444
13800
|
}
|
|
13445
|
-
let name = (0,
|
|
13801
|
+
let name = (0, import_node_path10.basename)(path);
|
|
13446
13802
|
let source = path;
|
|
13447
13803
|
let cleanup = async () => void 0;
|
|
13448
13804
|
let tier;
|
|
@@ -13468,7 +13824,7 @@ async function openSource(path, mime, requestedTier) {
|
|
|
13468
13824
|
}
|
|
13469
13825
|
let fileBlob;
|
|
13470
13826
|
try {
|
|
13471
|
-
fileBlob = await (0,
|
|
13827
|
+
fileBlob = await (0, import_node_fs6.openAsBlob)(source);
|
|
13472
13828
|
} catch (err2) {
|
|
13473
13829
|
await cleanup();
|
|
13474
13830
|
throw failed("unable to open", err2);
|
|
@@ -13589,13 +13945,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13589
13945
|
// backend this seal is actually created on — recovery refuses to
|
|
13590
13946
|
// "revoke" against a different backend whose 404 would be misread
|
|
13591
13947
|
// as success.
|
|
13592
|
-
backend_url: ctx.client.baseUrl
|
|
13948
|
+
backend_url: ctx.client.baseUrl,
|
|
13949
|
+
// SPEC-ADDRESS F1 §4: another MCP host starting now must not recover
|
|
13950
|
+
// (revoke) this upload while it runs.
|
|
13951
|
+
owner: { pid: process.pid, hostname: (0, import_node_os8.hostname)() }
|
|
13593
13952
|
};
|
|
13594
|
-
ctx.state.pending[handle] = op;
|
|
13595
13953
|
try {
|
|
13596
|
-
await ctx
|
|
13954
|
+
await journal(ctx, handle, op);
|
|
13597
13955
|
} catch (err2) {
|
|
13598
|
-
delete ctx.state.pending[handle];
|
|
13599
13956
|
throw new ToolError(
|
|
13600
13957
|
"state_persist_failed",
|
|
13601
13958
|
`refusing to upload: could not journal the operation before its first remote effect: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13616,7 +13973,7 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13616
13973
|
// `uploaded` checkpoint after this call is the durability gate.
|
|
13617
13974
|
onSealCreated: async (sealId) => {
|
|
13618
13975
|
op.seal_id = sealId;
|
|
13619
|
-
await
|
|
13976
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13620
13977
|
}
|
|
13621
13978
|
});
|
|
13622
13979
|
} catch (err2) {
|
|
@@ -13625,15 +13982,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13625
13982
|
return awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, expiresAt, session });
|
|
13626
13983
|
}
|
|
13627
13984
|
if (op.seal_id !== void 0) {
|
|
13628
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13985
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13629
13986
|
throw new ToolError(
|
|
13630
13987
|
"upload_failed",
|
|
13631
13988
|
`seal upload failed after the seal was created: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13632
13989
|
err2
|
|
13633
13990
|
);
|
|
13634
13991
|
}
|
|
13635
|
-
|
|
13636
|
-
await persistBestEffort(ctx);
|
|
13992
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13637
13993
|
throw new ToolError(
|
|
13638
13994
|
"upload_failed",
|
|
13639
13995
|
`seal upload failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13646,9 +14002,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13646
14002
|
op.seal_id = uploaded.sealId;
|
|
13647
14003
|
op.step = "uploaded";
|
|
13648
14004
|
try {
|
|
13649
|
-
await ctx
|
|
14005
|
+
await journal(ctx, handle, op);
|
|
13650
14006
|
} catch (err2) {
|
|
13651
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
14007
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13652
14008
|
throw new ToolError(
|
|
13653
14009
|
"state_persist_failed",
|
|
13654
14010
|
`could not journal the live seal after upload: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13661,7 +14017,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13661
14017
|
try {
|
|
13662
14018
|
shareBlobBytes = await encryptShareBlob(fileKeysB64, shareLinkKey, void 0);
|
|
13663
14019
|
} catch (err2) {
|
|
13664
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
14020
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13665
14021
|
throw new ToolError(
|
|
13666
14022
|
"share_blob_failed",
|
|
13667
14023
|
`share blob encryption failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13669,17 +14025,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13669
14025
|
);
|
|
13670
14026
|
}
|
|
13671
14027
|
const shareBlobB64 = bytesToBase64url(shareBlobBytes);
|
|
13672
|
-
let shareCode;
|
|
13673
|
-
try {
|
|
13674
|
-
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13675
|
-
} catch (err2) {
|
|
13676
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13677
|
-
throw new ToolError(
|
|
13678
|
-
"share_link_failed",
|
|
13679
|
-
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13680
|
-
err2
|
|
13681
|
-
);
|
|
13682
|
-
}
|
|
13683
14028
|
let fragment;
|
|
13684
14029
|
if (mode === "forward") {
|
|
13685
14030
|
if (recipientPub === null) {
|
|
@@ -13691,7 +14036,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13691
14036
|
try {
|
|
13692
14037
|
fragment = await buildTargetedFragment(shareLinkKey, recipientPub, uploaded.sealId);
|
|
13693
14038
|
} catch (err2) {
|
|
13694
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
14039
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13695
14040
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
13696
14041
|
if (message.includes("non-contributory")) {
|
|
13697
14042
|
throw new ToolError(
|
|
@@ -13709,10 +14054,33 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13709
14054
|
} else {
|
|
13710
14055
|
fragment = bytesToBase64url(shareLinkKey);
|
|
13711
14056
|
}
|
|
14057
|
+
if (mode === "forward") await journalDelivering(ctx, handle, op);
|
|
14058
|
+
let shareCode;
|
|
14059
|
+
try {
|
|
14060
|
+
shareCode = mode === "forward" && recipientPub !== null ? await withAddressProof(
|
|
14061
|
+
ctx.client,
|
|
14062
|
+
ctx.identity.secret,
|
|
14063
|
+
void 0,
|
|
14064
|
+
(sender) => ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64, {
|
|
14065
|
+
to: bytesToBase64url(recipientPub),
|
|
14066
|
+
fragment,
|
|
14067
|
+
label: deps.label?.slice(0, 80),
|
|
14068
|
+
sender
|
|
14069
|
+
})
|
|
14070
|
+
) : await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
14071
|
+
} catch (err2) {
|
|
14072
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
14073
|
+
const appCode = err2 instanceof BackendError ? err2.appCode : void 0;
|
|
14074
|
+
throw new ToolError(
|
|
14075
|
+
appCode === "intake.inbox_full" ? "inbox_full" : appCode === "intake.proof_required" ? "inbox_proof_failed" : "share_link_failed",
|
|
14076
|
+
`POST /seal/${uploaded.sealId}/share failed: ${messageOf3(err2)} (${outcome})`,
|
|
14077
|
+
err2
|
|
14078
|
+
);
|
|
14079
|
+
}
|
|
13712
14080
|
const host = resolvePublicHost(deps.publicHost);
|
|
13713
14081
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13714
14082
|
if (mode === "forward") {
|
|
13715
|
-
|
|
14083
|
+
await complete(ctx, handle, {
|
|
13716
14084
|
seal_id: uploaded.sealId,
|
|
13717
14085
|
owner_token_b64: ownerTokenB64,
|
|
13718
14086
|
created_at: nowIso2(ctx),
|
|
@@ -13720,41 +14088,29 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13720
14088
|
expires_at: expiresAt,
|
|
13721
14089
|
backend_url: ctx.client.baseUrl
|
|
13722
14090
|
});
|
|
13723
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13724
|
-
delete ctx.state.pending[handle];
|
|
13725
|
-
await ctx.persist();
|
|
13726
14091
|
return {
|
|
13727
14092
|
handle,
|
|
13728
14093
|
shareUrl,
|
|
13729
14094
|
expiresAt,
|
|
13730
14095
|
maxReads: FORWARD_MAX_READS_FORCED,
|
|
13731
|
-
mode: "forward"
|
|
14096
|
+
mode: "forward",
|
|
14097
|
+
delivered: true
|
|
13732
14098
|
};
|
|
13733
14099
|
}
|
|
13734
|
-
op.step = "delivering";
|
|
13735
14100
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13736
|
-
|
|
13737
|
-
await ctx.persist();
|
|
13738
|
-
} catch (err2) {
|
|
13739
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13740
|
-
throw new ToolError(
|
|
13741
|
-
"state_persist_failed",
|
|
13742
|
-
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13743
|
-
err2
|
|
13744
|
-
);
|
|
13745
|
-
}
|
|
14101
|
+
await journalDelivering(ctx, handle, op);
|
|
13746
14102
|
let handoff;
|
|
13747
14103
|
try {
|
|
13748
14104
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
13749
14105
|
} catch (err2) {
|
|
13750
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
14106
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13751
14107
|
throw new ToolError(
|
|
13752
14108
|
"handoff_both_channels_failed",
|
|
13753
14109
|
`both clipboard and file handoff channels failed \u2014 ${outcome}`,
|
|
13754
14110
|
err2
|
|
13755
14111
|
);
|
|
13756
14112
|
}
|
|
13757
|
-
|
|
14113
|
+
await complete(ctx, handle, {
|
|
13758
14114
|
seal_id: uploaded.sealId,
|
|
13759
14115
|
owner_token_b64: ownerTokenB64,
|
|
13760
14116
|
created_at: nowIso2(ctx),
|
|
@@ -13763,9 +14119,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13763
14119
|
handoff_channel: handoff.channel,
|
|
13764
14120
|
backend_url: ctx.client.baseUrl
|
|
13765
14121
|
});
|
|
13766
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13767
|
-
delete ctx.state.pending[handle];
|
|
13768
|
-
await ctx.persist();
|
|
13769
14122
|
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13770
14123
|
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.`;
|
|
13771
14124
|
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;
|
|
@@ -13780,16 +14133,29 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13780
14133
|
function messageOf3(err2) {
|
|
13781
14134
|
return err2 instanceof Error ? err2.message : String(err2);
|
|
13782
14135
|
}
|
|
14136
|
+
async function journalDelivering(ctx, handle, op) {
|
|
14137
|
+
op.step = "delivering";
|
|
14138
|
+
try {
|
|
14139
|
+
await journal(ctx, handle, op);
|
|
14140
|
+
} catch (err2) {
|
|
14141
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
14142
|
+
throw new ToolError(
|
|
14143
|
+
"state_persist_failed",
|
|
14144
|
+
`could not journal the delivery attempt before the link could reach anyone: ${messageOf3(err2)} (${outcome})`,
|
|
14145
|
+
err2
|
|
14146
|
+
);
|
|
14147
|
+
}
|
|
14148
|
+
}
|
|
13783
14149
|
async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, session }) {
|
|
13784
14150
|
const tier = prepared.tier;
|
|
13785
14151
|
const sealId = op.seal_id;
|
|
13786
14152
|
op.step = "awaiting_payment";
|
|
13787
|
-
await
|
|
14153
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13788
14154
|
let paymentUrl;
|
|
13789
14155
|
try {
|
|
13790
14156
|
paymentUrl = await ctx.client.createCheckout(sealId, ownerTokenB64, tier);
|
|
13791
14157
|
} catch (err2) {
|
|
13792
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
14158
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13793
14159
|
throw new ToolError("payment_failed", `POST /billing/checkout failed: ${messageOf3(err2)} (${outcome})`, err2);
|
|
13794
14160
|
}
|
|
13795
14161
|
const deadline = paymentDeadline(op.expires_at);
|
|
@@ -13799,7 +14165,7 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13799
14165
|
clearTimeout(timer);
|
|
13800
14166
|
ctx.payments?.delete(handle);
|
|
13801
14167
|
await prepared.cleanup();
|
|
13802
|
-
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle)})`);
|
|
14168
|
+
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle, op)})`);
|
|
13803
14169
|
};
|
|
13804
14170
|
const finish = async (onProgress) => {
|
|
13805
14171
|
if (running) throw new ToolError("payment_invalid", `share ${handle} is already being finished`);
|
|
@@ -13871,7 +14237,8 @@ function shareOutputToWire(out) {
|
|
|
13871
14237
|
share_url: out.shareUrl,
|
|
13872
14238
|
expires_at: out.expiresAt,
|
|
13873
14239
|
max_reads: out.maxReads,
|
|
13874
|
-
mode: out.mode
|
|
14240
|
+
mode: out.mode,
|
|
14241
|
+
delivered: out.delivered
|
|
13875
14242
|
};
|
|
13876
14243
|
}
|
|
13877
14244
|
const wire = {
|
|
@@ -13938,6 +14305,18 @@ function openOutputToWire(out) {
|
|
|
13938
14305
|
expires_at: out.expiresAt
|
|
13939
14306
|
};
|
|
13940
14307
|
}
|
|
14308
|
+
function inboxOutputToWire(out) {
|
|
14309
|
+
return {
|
|
14310
|
+
address_emoji: out.addressEmoji,
|
|
14311
|
+
items: out.items.map((item) => ({
|
|
14312
|
+
handle: item.handle,
|
|
14313
|
+
from: item.from,
|
|
14314
|
+
label: item.label,
|
|
14315
|
+
received_at: item.receivedAt,
|
|
14316
|
+
expires_at: item.expiresAt
|
|
14317
|
+
}))
|
|
14318
|
+
};
|
|
14319
|
+
}
|
|
13941
14320
|
function listOutputToWire(out) {
|
|
13942
14321
|
return {
|
|
13943
14322
|
entries: out.entries.map((entry) => ({
|
|
@@ -13968,7 +14347,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13968
14347
|
}
|
|
13969
14348
|
|
|
13970
14349
|
// src/server.ts
|
|
13971
|
-
var SERVER_VERSION = "0.
|
|
14350
|
+
var SERVER_VERSION = "0.4.0";
|
|
13972
14351
|
var toolMeta = (name) => {
|
|
13973
14352
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13974
14353
|
return { title, description, annotations };
|
|
@@ -13976,6 +14355,7 @@ var toolMeta = (name) => {
|
|
|
13976
14355
|
var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
|
|
13977
14356
|
var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
|
|
13978
14357
|
var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
|
|
14358
|
+
var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
|
|
13979
14359
|
var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
|
|
13980
14360
|
var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
|
|
13981
14361
|
var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
|
|
@@ -13995,44 +14375,33 @@ function progressReporter(extra) {
|
|
|
13995
14375
|
}
|
|
13996
14376
|
async function createServer(options) {
|
|
13997
14377
|
logEngineToStderr();
|
|
13998
|
-
let view;
|
|
13999
|
-
let identity;
|
|
14000
|
-
let lock = null;
|
|
14001
14378
|
const client = new SealApi({ baseUrl: resolveBackendUrl(options.backendBaseUrl) });
|
|
14002
14379
|
const disk = options.state;
|
|
14380
|
+
let view;
|
|
14381
|
+
const update = async (mutate) => {
|
|
14382
|
+
if (disk === null) await mutate(view);
|
|
14383
|
+
else view = await updateState(disk.passphrase, disk.path, mutate);
|
|
14384
|
+
};
|
|
14385
|
+
let identity;
|
|
14003
14386
|
if (disk === null) {
|
|
14004
14387
|
identity = await generateIdentity();
|
|
14005
14388
|
view = createEmptyState(identityToFields(identity));
|
|
14006
14389
|
} else {
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
try {
|
|
14010
|
-
view = await loadState(disk.passphrase, disk.path);
|
|
14011
|
-
identity = await loadIdentity(view.identity);
|
|
14012
|
-
const { changed } = await recoverPendingOperations(view, client);
|
|
14013
|
-
if (changed) {
|
|
14014
|
-
await saveState(view, disk.passphrase, disk.path);
|
|
14015
|
-
}
|
|
14016
|
-
} catch (err2) {
|
|
14017
|
-
await held.release();
|
|
14018
|
-
throw err2;
|
|
14019
|
-
}
|
|
14390
|
+
view = await updateState(disk.passphrase, disk.path, async (state) => (await recoverPendingOperations(state, client)).changed);
|
|
14391
|
+
identity = await loadIdentity(view.identity);
|
|
14020
14392
|
}
|
|
14021
14393
|
const publicHost = resolvePublicHost(options.publicHost);
|
|
14022
|
-
let persistChain = Promise.resolve();
|
|
14023
|
-
const persist = () => {
|
|
14024
|
-
if (disk === null) return Promise.resolve();
|
|
14025
|
-
persistChain = persistChain.catch(() => void 0).then(() => saveState(view, disk.passphrase, disk.path));
|
|
14026
|
-
return persistChain;
|
|
14027
|
-
};
|
|
14028
14394
|
const ctx = {
|
|
14029
|
-
state
|
|
14395
|
+
get state() {
|
|
14396
|
+
return view;
|
|
14397
|
+
},
|
|
14030
14398
|
identity,
|
|
14031
14399
|
client,
|
|
14032
|
-
|
|
14400
|
+
update,
|
|
14033
14401
|
ephemeral: disk === null,
|
|
14034
14402
|
requests: /* @__PURE__ */ new Map(),
|
|
14035
|
-
payments: /* @__PURE__ */ new Map()
|
|
14403
|
+
payments: /* @__PURE__ */ new Map(),
|
|
14404
|
+
inbox: { items: /* @__PURE__ */ new Map(), version: null }
|
|
14036
14405
|
};
|
|
14037
14406
|
const mcp = new import_mcp.McpServer(
|
|
14038
14407
|
{ name: "sealnet-mcp", version: SERVER_VERSION },
|
|
@@ -14079,6 +14448,7 @@ async function createServer(options) {
|
|
|
14079
14448
|
},
|
|
14080
14449
|
{
|
|
14081
14450
|
publicHost,
|
|
14451
|
+
label: client2.getClientVersion()?.name,
|
|
14082
14452
|
handoff: disableClipboard ? { disableClipboard: true } : void 0,
|
|
14083
14453
|
elicitUrl: urlMode ? async (params) => (await client2.elicitInput({ mode: "url", ...params })).action : void 0,
|
|
14084
14454
|
elicitationDone: urlMode ? (id) => client2.createElicitationCompletionNotifier(id)() : void 0
|
|
@@ -14132,19 +14502,37 @@ async function createServer(options) {
|
|
|
14132
14502
|
{
|
|
14133
14503
|
...toolMeta("seal_open"),
|
|
14134
14504
|
inputSchema: {
|
|
14135
|
-
url
|
|
14505
|
+
// Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
|
|
14506
|
+
url: external_exports.string().url().optional().describe(OPEN_INPUT.url.description),
|
|
14507
|
+
item: external_exports.string().regex(/^in_/).optional().describe(OPEN_INPUT.item.description),
|
|
14136
14508
|
mode: external_exports.enum(["metadata", "inline", "file"]).default("metadata").describe(OPEN_INPUT.mode.description),
|
|
14137
14509
|
dir: external_exports.string().optional().describe(OPEN_INPUT.dir.description)
|
|
14138
14510
|
}
|
|
14139
14511
|
},
|
|
14140
14512
|
async (args, extra) => {
|
|
14141
14513
|
const result = await wrapTool(
|
|
14142
|
-
() => sealOpen(ctx, {
|
|
14514
|
+
() => sealOpen(ctx, {
|
|
14515
|
+
url: args.url,
|
|
14516
|
+
item: args.item,
|
|
14517
|
+
mode: args.mode,
|
|
14518
|
+
dir: args.dir,
|
|
14519
|
+
onProgress: progressReporter(extra)
|
|
14520
|
+
}),
|
|
14143
14521
|
openOutputToWire
|
|
14144
14522
|
);
|
|
14145
14523
|
return result;
|
|
14146
14524
|
}
|
|
14147
14525
|
);
|
|
14526
|
+
mcp.registerTool(
|
|
14527
|
+
"seal_inbox",
|
|
14528
|
+
{
|
|
14529
|
+
...toolMeta("seal_inbox"),
|
|
14530
|
+
inputSchema: {
|
|
14531
|
+
wait: external_exports.number().int().min(0).max(300).default(0).describe(INBOX_INPUT.wait.description)
|
|
14532
|
+
}
|
|
14533
|
+
},
|
|
14534
|
+
async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
|
|
14535
|
+
);
|
|
14148
14536
|
mcp.registerTool(
|
|
14149
14537
|
"seal_list",
|
|
14150
14538
|
{
|
|
@@ -14232,18 +14620,12 @@ async function createServer(options) {
|
|
|
14232
14620
|
);
|
|
14233
14621
|
}
|
|
14234
14622
|
const transport = new import_stdio.StdioServerTransport();
|
|
14235
|
-
|
|
14236
|
-
await mcp.connect(transport);
|
|
14237
|
-
} catch (err2) {
|
|
14238
|
-
await lock?.release();
|
|
14239
|
-
throw err2;
|
|
14240
|
-
}
|
|
14623
|
+
await mcp.connect(transport);
|
|
14241
14624
|
const close = async () => {
|
|
14242
14625
|
try {
|
|
14243
14626
|
await mcp.close();
|
|
14244
14627
|
} finally {
|
|
14245
14628
|
await transport.close().catch(() => void 0);
|
|
14246
|
-
await lock?.release();
|
|
14247
14629
|
}
|
|
14248
14630
|
};
|
|
14249
14631
|
return { mcp, transport, close };
|
|
@@ -14257,7 +14639,7 @@ async function wrapTool(fn, toWire) {
|
|
|
14257
14639
|
structuredContent: wire
|
|
14258
14640
|
};
|
|
14259
14641
|
} catch (err2) {
|
|
14260
|
-
if (err2 instanceof ToolError) {
|
|
14642
|
+
if (err2 instanceof ToolError || err2 instanceof StateBusyError) {
|
|
14261
14643
|
return {
|
|
14262
14644
|
isError: true,
|
|
14263
14645
|
content: [
|
|
@@ -14279,7 +14661,6 @@ async function wrapTool(fn, toWire) {
|
|
|
14279
14661
|
// src/cli.ts
|
|
14280
14662
|
var PROGRAM_NAME = "sealnet-mcp";
|
|
14281
14663
|
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.";
|
|
14282
|
-
var MIN_PASSPHRASE_LEN = 8;
|
|
14283
14664
|
function logStderr(line) {
|
|
14284
14665
|
process.stderr.write(`${line}
|
|
14285
14666
|
`);
|
|
@@ -14288,118 +14669,33 @@ function logStdout(line) {
|
|
|
14288
14669
|
process.stdout.write(`${line}
|
|
14289
14670
|
`);
|
|
14290
14671
|
}
|
|
14291
|
-
async function
|
|
14292
|
-
|
|
14293
|
-
await (0, import_promises10.stat)(p);
|
|
14294
|
-
return true;
|
|
14295
|
-
} catch {
|
|
14296
|
-
return false;
|
|
14297
|
-
}
|
|
14672
|
+
async function agentKey(path) {
|
|
14673
|
+
return await readSeed(path.dir) ?? (await openAgentState(path)).root;
|
|
14298
14674
|
}
|
|
14299
14675
|
async function runInit(opts) {
|
|
14300
14676
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14301
|
-
if (
|
|
14677
|
+
if (opts.force) await withStateLock(path.dir, () => import_node_fs7.promises.rm(path.statePath, { force: true }));
|
|
14678
|
+
const opened = await openAgentState(path);
|
|
14679
|
+
if (!opened.created) {
|
|
14302
14680
|
logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
|
|
14303
|
-
logStderr("Re-run with --force to
|
|
14681
|
+
logStderr("Re-run with --force to replace it with an empty one (its handles are lost; the address stays).");
|
|
14304
14682
|
process.exit(2);
|
|
14305
14683
|
}
|
|
14306
|
-
const
|
|
14307
|
-
let passphrase;
|
|
14308
|
-
if (fromEnv !== void 0 && fromEnv.length > 0) {
|
|
14309
|
-
passphrase = fromEnv;
|
|
14310
|
-
logStderr(
|
|
14311
|
-
`${PROGRAM_NAME} init: using passphrase from ${PASSPHRASE_ENV_VAR} (will also save to OS keychain).`
|
|
14312
|
-
);
|
|
14313
|
-
} else {
|
|
14314
|
-
passphrase = await promptHiddenPassphrase("Choose a passphrase (will not echo): ");
|
|
14315
|
-
if (passphrase.length < MIN_PASSPHRASE_LEN) {
|
|
14316
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
14317
|
-
}
|
|
14318
|
-
const confirm = await promptHiddenPassphrase("Confirm passphrase: ");
|
|
14319
|
-
if (confirm !== passphrase) {
|
|
14320
|
-
throw new Error("Passphrases do not match.");
|
|
14321
|
-
}
|
|
14322
|
-
}
|
|
14323
|
-
logStderr(`${PROGRAM_NAME} init: generating X25519 identity ...`);
|
|
14324
|
-
const identity = await generateIdentity();
|
|
14325
|
-
const identityFields = identityToFields(identity);
|
|
14326
|
-
const state = createEmptyState(identityFields);
|
|
14327
|
-
logStderr(`${PROGRAM_NAME} init: writing encrypted state to ${path.statePath} ...`);
|
|
14328
|
-
const lock = await acquireStateLock(path.dir);
|
|
14329
|
-
let keychainStored = false;
|
|
14330
|
-
try {
|
|
14331
|
-
if (!opts.force && await pathExists(path.statePath)) {
|
|
14332
|
-
logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
|
|
14333
|
-
logStderr("Re-run with --force to overwrite (you will lose existing handles + identity).");
|
|
14334
|
-
process.exit(2);
|
|
14335
|
-
}
|
|
14336
|
-
await saveState(state, passphrase, path.statePath);
|
|
14337
|
-
try {
|
|
14338
|
-
await setKeychainPassphrase(passphrase);
|
|
14339
|
-
keychainStored = true;
|
|
14340
|
-
logStderr(
|
|
14341
|
-
`${PROGRAM_NAME} init: passphrase stored in OS keychain (service=${KEYCHAIN_SERVICE} account=${KEYCHAIN_ACCOUNT}).`
|
|
14342
|
-
);
|
|
14343
|
-
} catch (err2) {
|
|
14344
|
-
if (err2 instanceof KeytarUnavailableError) {
|
|
14345
|
-
logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
|
|
14346
|
-
logStderr(
|
|
14347
|
-
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
|
|
14348
|
-
);
|
|
14349
|
-
} else {
|
|
14350
|
-
throw err2;
|
|
14351
|
-
}
|
|
14352
|
-
}
|
|
14353
|
-
} finally {
|
|
14354
|
-
await lock.release();
|
|
14355
|
-
}
|
|
14684
|
+
const identity = await identityFromSeed(opened.root.seed);
|
|
14356
14685
|
logStdout(`${PROGRAM_NAME} init: ready.`);
|
|
14357
|
-
logStdout(` state.json:
|
|
14358
|
-
logStdout(`
|
|
14359
|
-
logStdout(`
|
|
14686
|
+
logStdout(` state.json: ${path.statePath}`);
|
|
14687
|
+
logStdout(` key source: ${opened.root.source}`);
|
|
14688
|
+
logStdout(` address: ${Buffer.from(identity.public).toString("base64url")}`);
|
|
14360
14689
|
logStdout("");
|
|
14361
|
-
logStdout(
|
|
14362
|
-
"Share the pubkey with senders so they can target SEALs to your MCP server (see `seal send --to <pubkey>` from `seal-cli`)."
|
|
14363
|
-
);
|
|
14364
|
-
}
|
|
14365
|
-
async function createStateWithoutPrompt(path) {
|
|
14366
|
-
const resolved = await passphraseForNewState();
|
|
14367
|
-
if (resolved === null) return null;
|
|
14368
|
-
const lock = await acquireStateLock(path.dir);
|
|
14369
|
-
try {
|
|
14370
|
-
if (await pathExists(path.statePath)) return void 0;
|
|
14371
|
-
if (resolved.fresh) await setKeychainPassphrase(resolved.passphrase);
|
|
14372
|
-
const identity = await generateIdentity();
|
|
14373
|
-
await saveState(createEmptyState(identityToFields(identity)), resolved.passphrase, path.statePath);
|
|
14374
|
-
} finally {
|
|
14375
|
-
await lock.release();
|
|
14376
|
-
}
|
|
14377
|
-
logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
|
|
14378
|
-
return resolved;
|
|
14690
|
+
logStdout("Senders use the address to deliver a link only this agent opens (`seal send --to <address>`).");
|
|
14379
14691
|
}
|
|
14380
14692
|
async function stateForServe(path) {
|
|
14381
|
-
|
|
14382
|
-
const created = await createStateWithoutPrompt(path);
|
|
14383
|
-
if (created !== void 0) return created;
|
|
14384
|
-
}
|
|
14385
|
-
const resolved = await resolvePassphraseFromStores();
|
|
14386
|
-
if (resolved === null) {
|
|
14387
|
-
throw new PassphraseNotFoundError();
|
|
14388
|
-
}
|
|
14389
|
-
if (resolved.source === "env") {
|
|
14390
|
-
logStderr(
|
|
14391
|
-
`${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\`).`
|
|
14392
|
-
);
|
|
14393
|
-
}
|
|
14693
|
+
let opened;
|
|
14394
14694
|
try {
|
|
14395
|
-
|
|
14396
|
-
await loadIdentity(view.identity);
|
|
14695
|
+
opened = await openAgentState(path);
|
|
14397
14696
|
} catch (err2) {
|
|
14398
|
-
if (err2 instanceof
|
|
14399
|
-
logStderr(`${PROGRAM_NAME} serve:
|
|
14400
|
-
logStderr(
|
|
14401
|
-
`If you reset your OS keychain, re-run \`${PROGRAM_NAME} init --force\` (you will lose existing handles).`
|
|
14402
|
-
);
|
|
14697
|
+
if (err2 instanceof IdentityConflictError) {
|
|
14698
|
+
logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
|
|
14403
14699
|
process.exit(3);
|
|
14404
14700
|
}
|
|
14405
14701
|
if (err2 instanceof StateCorrupt) {
|
|
@@ -14408,29 +14704,27 @@ async function stateForServe(path) {
|
|
|
14408
14704
|
}
|
|
14409
14705
|
throw err2;
|
|
14410
14706
|
}
|
|
14411
|
-
|
|
14707
|
+
if (opened.created) logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
|
|
14708
|
+
if (opened.moved !== null) logStderr(`${PROGRAM_NAME} serve: moved the 0.2 state under the agent's key (${opened.root.source})`);
|
|
14709
|
+
if (opened.moved === "env") {
|
|
14710
|
+
logStderr(
|
|
14711
|
+
`${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).`
|
|
14712
|
+
);
|
|
14713
|
+
}
|
|
14714
|
+
return opened;
|
|
14412
14715
|
}
|
|
14413
14716
|
async function runServe(opts) {
|
|
14414
14717
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14415
|
-
const
|
|
14718
|
+
const opened = opts.ephemeral || process.env[EPHEMERAL_ENV_VAR] === "1" ? null : await stateForServe(path);
|
|
14416
14719
|
logStderr(
|
|
14417
|
-
|
|
14720
|
+
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})`
|
|
14418
14721
|
);
|
|
14419
14722
|
logStderr(`${PROGRAM_NAME} serve: starting stdio MCP server ...`);
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
publicHost: opts.publicHost
|
|
14426
|
-
});
|
|
14427
|
-
} catch (err2) {
|
|
14428
|
-
if (err2 instanceof StateLockedError) {
|
|
14429
|
-
logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
|
|
14430
|
-
process.exit(5);
|
|
14431
|
-
}
|
|
14432
|
-
throw err2;
|
|
14433
|
-
}
|
|
14723
|
+
const handle = await createServer({
|
|
14724
|
+
state: opened === null ? null : { path: path.statePath, passphrase: opened.passphrase },
|
|
14725
|
+
backendBaseUrl: opts.backendUrl,
|
|
14726
|
+
publicHost: opts.publicHost
|
|
14727
|
+
});
|
|
14434
14728
|
const shutdown = async (signal) => {
|
|
14435
14729
|
logStderr(`${PROGRAM_NAME} serve: received ${signal}, shutting down ...`);
|
|
14436
14730
|
try {
|
|
@@ -14449,28 +14743,39 @@ async function runServe(opts) {
|
|
|
14449
14743
|
});
|
|
14450
14744
|
}
|
|
14451
14745
|
async function runPubkey(opts) {
|
|
14452
|
-
const
|
|
14453
|
-
|
|
14454
|
-
|
|
14746
|
+
const root = await agentKey(resolveStateDir({ cliFlag: opts.stateDir }));
|
|
14747
|
+
const { public: address } = await identityFromSeed(root.seed);
|
|
14748
|
+
logStdout(Buffer.from(address).toString("base64url"));
|
|
14749
|
+
logStderr(`${keyEmoji(address)} key: ${root.source}`);
|
|
14750
|
+
}
|
|
14751
|
+
async function runSeed(opts) {
|
|
14752
|
+
if (!opts.force) {
|
|
14753
|
+
throw new Error(`seed prints the agent's secret key \u2014 re-run with --force to confirm, to set ${SEED_ENV_VAR} elsewhere.`);
|
|
14455
14754
|
}
|
|
14456
|
-
const
|
|
14457
|
-
|
|
14458
|
-
|
|
14459
|
-
|
|
14755
|
+
const root = await agentKey(resolveStateDir({ cliFlag: opts.stateDir }));
|
|
14756
|
+
logStderr(
|
|
14757
|
+
`${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.`
|
|
14758
|
+
);
|
|
14759
|
+
logStdout(Buffer.from(root.seed).toString("base64url"));
|
|
14760
|
+
}
|
|
14761
|
+
async function perms(file) {
|
|
14762
|
+
return (0, import_promises10.stat)(file).then(
|
|
14763
|
+
(st) => (st.mode & 511).toString(8).padStart(3, "0"),
|
|
14764
|
+
() => null
|
|
14765
|
+
);
|
|
14460
14766
|
}
|
|
14461
14767
|
async function runDoctor(opts) {
|
|
14462
14768
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14463
|
-
const
|
|
14464
|
-
let
|
|
14465
|
-
let
|
|
14466
|
-
let stateOwnerUid = null;
|
|
14769
|
+
const seedFile = (0, import_node_path11.join)(path.dir, SEED_FILE_NAME);
|
|
14770
|
+
let seedSource = null;
|
|
14771
|
+
let seedError = null;
|
|
14467
14772
|
try {
|
|
14468
|
-
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
stateOwnerUid = typeof st.uid === "number" ? st.uid : null;
|
|
14472
|
-
} catch {
|
|
14773
|
+
seedSource = (await readSeed(path.dir))?.source ?? null;
|
|
14774
|
+
} catch (err2) {
|
|
14775
|
+
seedError = err2 instanceof Error ? err2.message : String(err2);
|
|
14473
14776
|
}
|
|
14777
|
+
const statePerms = await perms(path.statePath);
|
|
14778
|
+
const seedPerms = await perms(seedFile);
|
|
14474
14779
|
const kc = await probeKeychain();
|
|
14475
14780
|
const lockProbe = await probeStateLock(path.dir);
|
|
14476
14781
|
const client = new SealApi({ baseUrl: resolveBackendUrl(opts.backendUrl) });
|
|
@@ -14491,103 +14796,101 @@ async function runDoctor(opts) {
|
|
|
14491
14796
|
backendReachable = false;
|
|
14492
14797
|
backendError = err2 instanceof Error ? err2.message : String(err2);
|
|
14493
14798
|
}
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14799
|
+
logStdout(
|
|
14800
|
+
JSON.stringify(
|
|
14801
|
+
{
|
|
14802
|
+
state_path: path.statePath,
|
|
14803
|
+
state_exists: statePerms !== null,
|
|
14804
|
+
state_perms_octal: statePerms,
|
|
14805
|
+
seed_source: seedSource,
|
|
14806
|
+
seed_error: seedError,
|
|
14807
|
+
seed_file_perms_octal: seedPerms,
|
|
14808
|
+
keychain_available: kc.available,
|
|
14809
|
+
keychain_has_seed: kc.hasSeed,
|
|
14810
|
+
env_seed_set: Boolean(process.env[SEED_ENV_VAR]),
|
|
14811
|
+
env_passphrase_set: Boolean(process.env[PASSPHRASE_ENV_VAR]),
|
|
14812
|
+
lock_held: lockProbe.held,
|
|
14813
|
+
lock_holder_pid: lockProbe.holder?.pid ?? null,
|
|
14814
|
+
lock_stale: lockProbe.stale,
|
|
14815
|
+
backend_url: client.baseUrl,
|
|
14816
|
+
public_host: resolvePublicHost(opts.publicHost),
|
|
14817
|
+
backend_reachable: backendReachable,
|
|
14818
|
+
backend_status: backendStatus,
|
|
14819
|
+
backend_error: backendError,
|
|
14820
|
+
platform: process.platform,
|
|
14821
|
+
node_version: process.version
|
|
14822
|
+
},
|
|
14823
|
+
null,
|
|
14824
|
+
2
|
|
14825
|
+
)
|
|
14826
|
+
);
|
|
14827
|
+
if (seedError !== null) logStderr(`hint: ${seedError}`);
|
|
14828
|
+
else if (seedSource === null) {
|
|
14829
|
+
logStderr(`hint: no agent key yet \u2014 the first \`${PROGRAM_NAME} serve\` makes it (${kc.available ? "in the OS keychain" : seedFile}).`);
|
|
14518
14830
|
}
|
|
14519
|
-
|
|
14520
|
-
logStderr(
|
|
14521
|
-
`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).`
|
|
14522
|
-
);
|
|
14523
|
-
} else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
|
|
14524
|
-
logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
|
|
14831
|
+
for (const [file, got] of [[path.statePath, statePerms], [seedFile, seedPerms]]) {
|
|
14832
|
+
if (got !== null && got !== "600") logStderr(`hint: ${file} perms are ${got}, expected 600. Run \`chmod 600 \u2026\`.`);
|
|
14525
14833
|
}
|
|
14526
14834
|
if (backendReachable === false) {
|
|
14527
14835
|
logStderr(`hint: backend ${client.baseUrl} unreachable (${backendError ?? "unknown"}).`);
|
|
14528
14836
|
}
|
|
14529
|
-
if (lockProbe.held &&
|
|
14530
|
-
logStderr(
|
|
14531
|
-
`hint: state dir is locked by pid ${lockProbe.holder?.pid ?? "?"} \u2014 a serve instance is running.`
|
|
14532
|
-
);
|
|
14533
|
-
} else if (lockProbe.held && lockProbe.stale) {
|
|
14534
|
-
logStderr("hint: stale state.lock detected (holder dead) \u2014 next serve/init will break it automatically.");
|
|
14837
|
+
if (lockProbe.held && lockProbe.stale) {
|
|
14838
|
+
logStderr("hint: stale state.lock detected (holder dead) \u2014 the next write breaks it automatically.");
|
|
14535
14839
|
}
|
|
14536
14840
|
}
|
|
14537
14841
|
async function runReset(opts) {
|
|
14538
14842
|
if (!opts.force) {
|
|
14539
14843
|
throw new Error(
|
|
14540
|
-
"reset is destructive \u2014 re-run with --force to confirm. This wipes state.json AND
|
|
14844
|
+
"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."
|
|
14541
14845
|
);
|
|
14542
14846
|
}
|
|
14543
14847
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14544
|
-
const
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
const code = err2.code;
|
|
14553
|
-
if (code !== "ENOENT") {
|
|
14554
|
-
throw err2;
|
|
14555
|
-
}
|
|
14556
|
-
}
|
|
14557
|
-
keychainDeleted = await deleteKeychainPassphrase();
|
|
14558
|
-
} finally {
|
|
14559
|
-
await lock.release();
|
|
14560
|
-
}
|
|
14848
|
+
const result = await withStateLock(path.dir, async () => {
|
|
14849
|
+
let stateDeleted = true;
|
|
14850
|
+
await import_node_fs7.promises.unlink(path.statePath).catch((err2) => {
|
|
14851
|
+
if (err2.code !== "ENOENT") throw err2;
|
|
14852
|
+
stateDeleted = false;
|
|
14853
|
+
});
|
|
14854
|
+
return { stateDeleted, ...await deleteRootSecret(path.dir) };
|
|
14855
|
+
});
|
|
14561
14856
|
logStdout(
|
|
14562
14857
|
JSON.stringify(
|
|
14563
14858
|
{
|
|
14564
|
-
state_deleted: stateDeleted,
|
|
14565
|
-
|
|
14859
|
+
state_deleted: result.stateDeleted,
|
|
14860
|
+
seed_file_deleted: result.seedFile,
|
|
14861
|
+
keychain_deleted: result.keychain,
|
|
14566
14862
|
state_path: path.statePath
|
|
14567
14863
|
},
|
|
14568
14864
|
null,
|
|
14569
14865
|
2
|
|
14570
14866
|
)
|
|
14571
14867
|
);
|
|
14868
|
+
logStderr(
|
|
14869
|
+
`${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.`
|
|
14870
|
+
);
|
|
14871
|
+
if (process.env[SEED_ENV_VAR]) {
|
|
14872
|
+
logStderr(`${PROGRAM_NAME} reset: ${SEED_ENV_VAR} is still set: the next start uses the key from the environment.`);
|
|
14873
|
+
}
|
|
14572
14874
|
}
|
|
14573
14875
|
function buildCli() {
|
|
14574
14876
|
const program = new import_commander.Command();
|
|
14575
14877
|
program.name(PROGRAM_NAME).description(DESCRIPTION).version(SERVER_VERSION, "-V, --version", `Print the ${PROGRAM_NAME} version and exit.`);
|
|
14576
14878
|
program.command("init").description(
|
|
14577
|
-
"
|
|
14578
|
-
).option("--state-dir <path>", "Override default state directory").option("--force", "
|
|
14879
|
+
"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)."
|
|
14880
|
+
).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));
|
|
14579
14881
|
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(
|
|
14580
14882
|
"--public-host <url>",
|
|
14581
14883
|
"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."
|
|
14582
|
-
).option("--ephemeral",
|
|
14884
|
+
).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));
|
|
14583
14885
|
program.command("pubkey").description(
|
|
14584
|
-
"Print
|
|
14886
|
+
"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."
|
|
14585
14887
|
).option("--state-dir <path>", "Override default state directory").action(async (opts) => runPubkey(opts));
|
|
14888
|
+
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));
|
|
14586
14889
|
program.command("doctor").description(
|
|
14587
|
-
"Diagnose keychain availability,
|
|
14890
|
+
"Diagnose the agent's key, keychain availability, file permissions and backend reachability. Prints a JSON report to stdout + hints to stderr."
|
|
14588
14891
|
).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));
|
|
14589
14892
|
program.command("reset").description(
|
|
14590
|
-
"Wipe state.json
|
|
14893
|
+
"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."
|
|
14591
14894
|
).option("--state-dir <path>", "Override default state directory").option("--force", "Confirm destructive operation", false).action(async (opts) => runReset(opts));
|
|
14592
14895
|
return program;
|
|
14593
14896
|
}
|