sealnet-mcp 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -17
- package/dist/cli.cjs +980 -997
- package/dist/cli.d.cts +10 -7
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +3719 -3736
- package/dist/index.cjs +357 -276
- package/dist/index.d.cts +662 -178
- package/dist/index.d.ts +662 -178
- package/dist/index.js +335 -262
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -35,16 +35,20 @@ __export(index_exports, {
|
|
|
35
35
|
BackendError: () => BackendError,
|
|
36
36
|
DEFAULT_BACKEND_URL: () => DEFAULT_BACKEND_URL,
|
|
37
37
|
DEFAULT_PUBLIC_HOST: () => DEFAULT_PUBLIC_HOST,
|
|
38
|
+
EPHEMERAL_ENV_VAR: () => EPHEMERAL_ENV_VAR,
|
|
38
39
|
HandoffBothChannelsFailedError: () => HandoffBothChannelsFailedError,
|
|
40
|
+
IdentityConflictError: () => IdentityConflictError,
|
|
39
41
|
KEYCHAIN_ACCOUNT: () => KEYCHAIN_ACCOUNT,
|
|
40
42
|
KEYCHAIN_SERVICE: () => KEYCHAIN_SERVICE,
|
|
41
|
-
KeytarUnavailableError: () => KeytarUnavailableError,
|
|
42
43
|
LOCK_FILE_NAME: () => LOCK_FILE_NAME,
|
|
43
44
|
PASSPHRASE_ENV_VAR: () => PASSPHRASE_ENV_VAR,
|
|
44
45
|
PUBLIC_HOST_ENV_VAR: () => PUBLIC_HOST_ENV_VAR,
|
|
45
|
-
|
|
46
|
+
SEED_ENV_VAR: () => SEED_ENV_VAR,
|
|
47
|
+
SEED_FILE_NAME: () => SEED_FILE_NAME,
|
|
46
48
|
STATE_FILE_NAME: () => STATE_FILE_NAME,
|
|
47
49
|
SealApi: () => SealApi,
|
|
50
|
+
SeedInvalidError: () => SeedInvalidError,
|
|
51
|
+
StateBusyError: () => StateBusyError,
|
|
48
52
|
StateCorrupt: () => StateCorrupt,
|
|
49
53
|
StateLockedError: () => StateLockedError,
|
|
50
54
|
StatePassphraseInvalid: () => StatePassphraseInvalid,
|
|
@@ -52,20 +56,22 @@ __export(index_exports, {
|
|
|
52
56
|
acquireStateLock: () => acquireStateLock,
|
|
53
57
|
createEmptyState: () => createEmptyState,
|
|
54
58
|
createServer: () => createServer,
|
|
55
|
-
|
|
59
|
+
deleteRootSecret: () => deleteRootSecret,
|
|
56
60
|
generateHandle: () => generateHandle,
|
|
57
61
|
generateIdentity: () => generateIdentity,
|
|
62
|
+
holderMayRun: () => holderMayRun,
|
|
63
|
+
identityFromSeed: () => identityFromSeed,
|
|
58
64
|
identityToFields: () => identityToFields,
|
|
59
65
|
isValidHandle: () => isValidHandle,
|
|
60
66
|
loadIdentity: () => loadIdentity,
|
|
61
67
|
loadState: () => loadState,
|
|
68
|
+
openAgentState: () => openAgentState,
|
|
62
69
|
performHandoff: () => performHandoff,
|
|
63
70
|
probeKeychain: () => probeKeychain,
|
|
64
71
|
probeStateLock: () => probeStateLock,
|
|
65
|
-
|
|
72
|
+
readSeed: () => readSeed,
|
|
66
73
|
recoverPendingOperations: () => recoverPendingOperations,
|
|
67
74
|
resolveBackendUrl: () => resolveBackendUrl,
|
|
68
|
-
resolvePassphraseFromStores: () => resolvePassphraseFromStores,
|
|
69
75
|
resolvePublicHost: () => resolvePublicHost,
|
|
70
76
|
resolveStateDir: () => resolveStateDir,
|
|
71
77
|
saveState: () => saveState,
|
|
@@ -73,12 +79,13 @@ __export(index_exports, {
|
|
|
73
79
|
sealOpen: () => sealOpen,
|
|
74
80
|
sealRevoke: () => sealRevoke,
|
|
75
81
|
sealShare: () => sealShare,
|
|
76
|
-
|
|
82
|
+
statePassphrase: () => statePassphrase,
|
|
83
|
+
updateState: () => updateState,
|
|
84
|
+
withStateLock: () => withStateLock
|
|
77
85
|
});
|
|
78
86
|
module.exports = __toCommonJS(index_exports);
|
|
79
87
|
|
|
80
88
|
// src/server.ts
|
|
81
|
-
var import_node_path9 = require("path");
|
|
82
89
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
83
90
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
84
91
|
|
|
@@ -4138,13 +4145,13 @@ var seal_mcp_v1_default = {
|
|
|
4138
4145
|
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`."
|
|
4139
4146
|
},
|
|
4140
4147
|
server_instructions: {
|
|
4141
|
-
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).",
|
|
4148
|
+
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.",
|
|
4142
4149
|
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."
|
|
4143
4150
|
},
|
|
4144
4151
|
tools: {
|
|
4145
4152
|
seal_share: {
|
|
4146
4153
|
title: "Share a file via SEAL",
|
|
4147
|
-
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for
|
|
4154
|
+
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for large files (folders, dumps, exports, builds, videos \u2014 up to 1 GB free, more after one card payment by the user). Default mode `handoff`: the link goes to the user's clipboard and you get a handle. Where there is no clipboard it goes to a 0600 file and you get its path: do not read that file and do not print it. Mode `forward`: a link only the holder of the given X25519 key can open \u2014 use it for another agent. Never paste secrets or huge content into the chat instead.",
|
|
4148
4155
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
4149
4156
|
input: {
|
|
4150
4157
|
properties: {
|
|
@@ -4188,13 +4195,15 @@ var seal_mcp_v1_default = {
|
|
|
4188
4195
|
required: ["handle", "receipt", "handoff_channel"],
|
|
4189
4196
|
properties: {
|
|
4190
4197
|
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." },
|
|
4191
|
-
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 `
|
|
4198
|
+
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." },
|
|
4192
4199
|
handoff_channel: { type: "string", enum: ["clipboard", "file"] },
|
|
4193
4200
|
handoff_file: { type: "string", description: "ONLY when handoff_channel='file'. Absolute path to a 0o600 file containing the share link and a newline." },
|
|
4194
4201
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
4195
4202
|
},
|
|
4196
4203
|
MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
|
|
4197
|
-
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
|
|
4204
|
+
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode.",
|
|
4205
|
+
receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
|
|
4206
|
+
receipt_file: "The seal link is in {file} (mode 0600, removed automatically later). Give it to the recipient. Anyone who can read files on this machine as this user, this agent included, can read it. In a cloud or container session prefer mode=forward to the recipient\u2019s key."
|
|
4198
4207
|
},
|
|
4199
4208
|
output_forward: {
|
|
4200
4209
|
type: "object",
|
|
@@ -4522,7 +4531,7 @@ var seal_mcp_v1_default = {
|
|
|
4522
4531
|
type: "object",
|
|
4523
4532
|
required: ["x25519_secret_b64", "x25519_public_b64"],
|
|
4524
4533
|
properties: {
|
|
4525
|
-
x25519_secret_b64: { type: "string", description: "32 raw bytes,
|
|
4534
|
+
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." },
|
|
4526
4535
|
x25519_public_b64: { type: "string", description: "32 raw bytes, derived from secret. Safe to publish; advertised so other parties can target this MCP." }
|
|
4527
4536
|
}
|
|
4528
4537
|
},
|
|
@@ -4543,7 +4552,7 @@ var seal_mcp_v1_default = {
|
|
|
4543
4552
|
},
|
|
4544
4553
|
pending: {
|
|
4545
4554
|
type: "object",
|
|
4546
|
-
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.",
|
|
4555
|
+
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.",
|
|
4547
4556
|
additionalProperties: {
|
|
4548
4557
|
type: "object",
|
|
4549
4558
|
required: ["tool", "mode", "started_at", "owner_token_b64", "expires_at", "step"],
|
|
@@ -4556,6 +4565,12 @@ var seal_mcp_v1_default = {
|
|
|
4556
4565
|
step: { type: "string", enum: ["preparing", "awaiting_payment", "uploaded", "delivering", "compensating"] },
|
|
4557
4566
|
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." },
|
|
4558
4567
|
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)." },
|
|
4568
|
+
owner: {
|
|
4569
|
+
type: "object",
|
|
4570
|
+
required: ["pid", "hostname"],
|
|
4571
|
+
properties: { pid: { type: "integer" }, hostname: { type: "string" } },
|
|
4572
|
+
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."
|
|
4573
|
+
},
|
|
4559
4574
|
compensation: {
|
|
4560
4575
|
type: "object",
|
|
4561
4576
|
required: ["action", "outcome", "error", "at"],
|
|
@@ -4587,27 +4602,20 @@ var seal_mcp_v1_default = {
|
|
|
4587
4602
|
}
|
|
4588
4603
|
}
|
|
4589
4604
|
},
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
default: true
|
|
4597
|
-
},
|
|
4598
|
-
{
|
|
4599
|
-
rank: 2,
|
|
4600
|
-
method: "env_var",
|
|
4601
|
-
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."
|
|
4602
|
-
},
|
|
4603
|
-
{
|
|
4604
|
-
rank: 3,
|
|
4605
|
-
method: "REFUSED",
|
|
4606
|
-
implementation: "Cleartext passphrase file on disk is explicitly refused even if user passes `--passphrase-file \u2026`. Error message points at options 1 and 2."
|
|
4607
|
-
}
|
|
4605
|
+
root_secret: {
|
|
4606
|
+
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.",
|
|
4607
|
+
ordered_sources: [
|
|
4608
|
+
{ rank: 1, source: "env", implementation: "$SEAL_SEED. How a container or a cloud session gets the same address as at home." },
|
|
4609
|
+
{ 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." },
|
|
4610
|
+
{ 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." }
|
|
4608
4611
|
],
|
|
4609
|
-
|
|
4610
|
-
|
|
4612
|
+
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.",
|
|
4613
|
+
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.",
|
|
4614
|
+
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.",
|
|
4615
|
+
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.",
|
|
4616
|
+
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.",
|
|
4617
|
+
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`.",
|
|
4618
|
+
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."
|
|
4611
4619
|
},
|
|
4612
4620
|
handoff_delivery: {
|
|
4613
4621
|
primary_channel: "system_clipboard",
|
|
@@ -4683,11 +4691,12 @@ var seal_mcp_v1_default = {
|
|
|
4683
4691
|
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.",
|
|
4684
4692
|
pro_failed: "seal_pro_*: SEAL Pro answered with another error; the message has the status.",
|
|
4685
4693
|
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.",
|
|
4694
|
+
state_busy: "Another sealnet-mcp process held the state lock for more than 5 s. Nothing was changed by this call; safe to retry.",
|
|
4686
4695
|
runtime_unsupported: "Node runtime is missing a global the bundle assumes (e.g. `globalThis.File` or `globalThis.Blob`). Upgrade to Node \u226520.",
|
|
4687
4696
|
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.",
|
|
4688
|
-
|
|
4697
|
+
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`.",
|
|
4689
4698
|
"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.",
|
|
4690
|
-
state_dir_unsafe: "
|
|
4699
|
+
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.",
|
|
4691
4700
|
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."
|
|
4692
4701
|
},
|
|
4693
4702
|
current_format_version: 1,
|
|
@@ -11511,7 +11520,7 @@ function safeFilename(name) {
|
|
|
11511
11520
|
return cleaned.length > 0 ? cleaned : "seal-file";
|
|
11512
11521
|
}
|
|
11513
11522
|
async function receiveToDisk(opened, dir, name) {
|
|
11514
|
-
const
|
|
11523
|
+
const fs3 = await import(
|
|
11515
11524
|
/* webpackIgnore: true */
|
|
11516
11525
|
/* @vite-ignore */
|
|
11517
11526
|
"fs/promises"
|
|
@@ -11521,10 +11530,10 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11521
11530
|
/* @vite-ignore */
|
|
11522
11531
|
"path"
|
|
11523
11532
|
);
|
|
11524
|
-
await
|
|
11533
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
11525
11534
|
const suffix = Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11526
11535
|
const tmp = path.join(dir, `.seal-partial-${suffix}`);
|
|
11527
|
-
const fh = await
|
|
11536
|
+
const fh = await fs3.open(tmp, "wx", 384);
|
|
11528
11537
|
let placed = false;
|
|
11529
11538
|
try {
|
|
11530
11539
|
await opened.stream.pipeTo(
|
|
@@ -11542,12 +11551,12 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11542
11551
|
for (let i = 0; i < 1e3; i++) {
|
|
11543
11552
|
const candidate = path.join(dir, i === 0 ? name : `${stem} (${i})${ext}`);
|
|
11544
11553
|
try {
|
|
11545
|
-
await
|
|
11554
|
+
await fs3.link(tmp, candidate);
|
|
11546
11555
|
} catch (err2) {
|
|
11547
11556
|
if (err2.code === "EEXIST") continue;
|
|
11548
11557
|
throw err2;
|
|
11549
11558
|
}
|
|
11550
|
-
await
|
|
11559
|
+
await fs3.unlink(tmp);
|
|
11551
11560
|
placed = true;
|
|
11552
11561
|
return { path: candidate, sizeBytes, sha256: sha2562 };
|
|
11553
11562
|
}
|
|
@@ -11555,7 +11564,7 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11555
11564
|
} finally {
|
|
11556
11565
|
if (!placed) {
|
|
11557
11566
|
await fh.close().catch(() => void 0);
|
|
11558
|
-
await
|
|
11567
|
+
await fs3.unlink(tmp).catch(() => void 0);
|
|
11559
11568
|
}
|
|
11560
11569
|
}
|
|
11561
11570
|
}
|
|
@@ -11993,6 +12002,9 @@ async function generateIdentity() {
|
|
|
11993
12002
|
const { publicKey, secretKey } = await generateRecipientKeyPair();
|
|
11994
12003
|
return { secret: secretKey, public: publicKey };
|
|
11995
12004
|
}
|
|
12005
|
+
async function identityFromSeed(seed) {
|
|
12006
|
+
return { secret: seed, public: await recipientPublicKey(seed) };
|
|
12007
|
+
}
|
|
11996
12008
|
function identityToFields(id) {
|
|
11997
12009
|
return {
|
|
11998
12010
|
x25519_secret_b64: toB64(id.secret),
|
|
@@ -12026,20 +12038,27 @@ var import_node_crypto = require("crypto");
|
|
|
12026
12038
|
var import_node_os = require("os");
|
|
12027
12039
|
var import_node_path = require("path");
|
|
12028
12040
|
var LOCK_FILE_NAME = "state.lock";
|
|
12041
|
+
var LOCK_WAIT_MS = 5e3;
|
|
12042
|
+
var LOCK_RETRY_MS = 25;
|
|
12029
12043
|
var StateLockedError = class extends Error {
|
|
12030
12044
|
code = "state_locked";
|
|
12031
12045
|
holder;
|
|
12032
12046
|
lockPath;
|
|
12033
12047
|
constructor(lockPath, holder) {
|
|
12034
12048
|
const who = holder === null ? "holder info unreadable" : `pid ${holder.pid} on ${holder.hostname} since ${holder.acquired_at}`;
|
|
12035
|
-
super(
|
|
12036
|
-
`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}`
|
|
12037
|
-
);
|
|
12049
|
+
super(`state directory is locked by another sealnet-mcp process (${who}). Lock file: ${lockPath}`);
|
|
12038
12050
|
this.name = "StateLockedError";
|
|
12039
12051
|
this.holder = holder;
|
|
12040
12052
|
this.lockPath = lockPath;
|
|
12041
12053
|
}
|
|
12042
12054
|
};
|
|
12055
|
+
var StateBusyError = class extends Error {
|
|
12056
|
+
code = "state_busy";
|
|
12057
|
+
constructor(locked) {
|
|
12058
|
+
super(`${locked.message}; waited ${LOCK_WAIT_MS / 1e3} s, nothing was changed, safe to retry`);
|
|
12059
|
+
this.name = "StateBusyError";
|
|
12060
|
+
}
|
|
12061
|
+
};
|
|
12043
12062
|
var StateDirUnsafeError = class extends Error {
|
|
12044
12063
|
code = "state_dir_unsafe";
|
|
12045
12064
|
constructor(stateDir, reason) {
|
|
@@ -12058,6 +12077,9 @@ function defaultIsPidAlive(pid) {
|
|
|
12058
12077
|
return code === "EPERM";
|
|
12059
12078
|
}
|
|
12060
12079
|
}
|
|
12080
|
+
function holderMayRun(holder, deps = {}) {
|
|
12081
|
+
return holder.hostname !== (deps.host ?? (0, import_node_os.hostname)()) || (deps.isPidAlive ?? defaultIsPidAlive)(holder.pid);
|
|
12082
|
+
}
|
|
12061
12083
|
function parseHolder(raw) {
|
|
12062
12084
|
try {
|
|
12063
12085
|
const parsed = JSON.parse(raw);
|
|
@@ -12100,7 +12122,7 @@ async function tryCreate(lockPath, info) {
|
|
|
12100
12122
|
await (0, import_promises2.unlink)(tmp).catch(() => void 0);
|
|
12101
12123
|
}
|
|
12102
12124
|
}
|
|
12103
|
-
async function tryBreakStale(lockPath, probed,
|
|
12125
|
+
async function tryBreakStale(lockPath, probed, deps) {
|
|
12104
12126
|
const claimPath = `${lockPath}.break-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}`;
|
|
12105
12127
|
try {
|
|
12106
12128
|
await (0, import_promises2.rename)(lockPath, claimPath);
|
|
@@ -12108,8 +12130,7 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
12108
12130
|
return true;
|
|
12109
12131
|
}
|
|
12110
12132
|
const claimed = parseHolder(await (0, import_promises2.readFile)(claimPath, "utf8").catch(() => ""));
|
|
12111
|
-
|
|
12112
|
-
if (sameHolder(claimed, probed) || claimedStale) {
|
|
12133
|
+
if (sameHolder(claimed, probed) || claimed === null || !holderMayRun(claimed, deps)) {
|
|
12113
12134
|
await (0, import_promises2.unlink)(claimPath).catch(() => void 0);
|
|
12114
12135
|
return true;
|
|
12115
12136
|
}
|
|
@@ -12122,11 +12143,9 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
12122
12143
|
}
|
|
12123
12144
|
async function acquireStateLock(stateDir, deps = {}) {
|
|
12124
12145
|
const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
|
|
12125
|
-
const isAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
12126
|
-
const selfHost = deps.host ?? (0, import_node_os.hostname)();
|
|
12127
12146
|
const self2 = {
|
|
12128
12147
|
pid: deps.pid ?? process.pid,
|
|
12129
|
-
hostname:
|
|
12148
|
+
hostname: deps.host ?? (0, import_node_os.hostname)(),
|
|
12130
12149
|
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12131
12150
|
};
|
|
12132
12151
|
await (0, import_promises2.mkdir)(stateDir, { recursive: true, mode: 448 });
|
|
@@ -12137,11 +12156,10 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
12137
12156
|
await (0, import_promises2.chmod)(stateDir, 448).catch(() => void 0);
|
|
12138
12157
|
if (!await tryCreate(lockPath, self2)) {
|
|
12139
12158
|
const holder = await readHolder(lockPath);
|
|
12140
|
-
|
|
12141
|
-
if (holder !== null && !stale) {
|
|
12159
|
+
if (holder !== null && holderMayRun(holder, deps)) {
|
|
12142
12160
|
throw new StateLockedError(lockPath, holder);
|
|
12143
12161
|
}
|
|
12144
|
-
if (!await tryBreakStale(lockPath, holder,
|
|
12162
|
+
if (!await tryBreakStale(lockPath, holder, deps)) {
|
|
12145
12163
|
throw new StateLockedError(lockPath, await readHolder(lockPath));
|
|
12146
12164
|
}
|
|
12147
12165
|
if (!await tryCreate(lockPath, self2)) {
|
|
@@ -12173,6 +12191,30 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
12173
12191
|
}
|
|
12174
12192
|
};
|
|
12175
12193
|
}
|
|
12194
|
+
var queues = /* @__PURE__ */ new Map();
|
|
12195
|
+
function withStateLock(stateDir, fn, deps = {}) {
|
|
12196
|
+
const run = (queues.get(stateDir) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
12197
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
12198
|
+
let lock;
|
|
12199
|
+
for (; ; ) {
|
|
12200
|
+
try {
|
|
12201
|
+
lock = await acquireStateLock(stateDir, deps);
|
|
12202
|
+
break;
|
|
12203
|
+
} catch (err2) {
|
|
12204
|
+
if (!(err2 instanceof StateLockedError)) throw err2;
|
|
12205
|
+
if (Date.now() >= deadline) throw new StateBusyError(err2);
|
|
12206
|
+
await new Promise((resolve5) => setTimeout(resolve5, LOCK_RETRY_MS));
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
try {
|
|
12210
|
+
return await fn();
|
|
12211
|
+
} finally {
|
|
12212
|
+
await lock.release();
|
|
12213
|
+
}
|
|
12214
|
+
});
|
|
12215
|
+
queues.set(stateDir, run);
|
|
12216
|
+
return run;
|
|
12217
|
+
}
|
|
12176
12218
|
async function probeStateLock(stateDir, deps = {}) {
|
|
12177
12219
|
const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
|
|
12178
12220
|
const holder = await readHolder(lockPath);
|
|
@@ -12184,10 +12226,7 @@ async function probeStateLock(stateDir, deps = {}) {
|
|
|
12184
12226
|
return { held: false, holder: null, stale: false };
|
|
12185
12227
|
}
|
|
12186
12228
|
}
|
|
12187
|
-
|
|
12188
|
-
const sameHost = holder.hostname === (deps.host ?? (0, import_node_os.hostname)());
|
|
12189
|
-
const stale = sameHost && !isAlive(holder.pid);
|
|
12190
|
-
return { held: true, holder, stale };
|
|
12229
|
+
return { held: true, holder, stale: !holderMayRun(holder, deps) };
|
|
12191
12230
|
}
|
|
12192
12231
|
|
|
12193
12232
|
// src/recovery.ts
|
|
@@ -12204,6 +12243,10 @@ async function recoverPendingOperations(view, client, opts = {}) {
|
|
|
12204
12243
|
log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
|
|
12205
12244
|
continue;
|
|
12206
12245
|
}
|
|
12246
|
+
if (op.owner !== void 0 && op.owner.pid !== process.pid && holderMayRun(op.owner)) {
|
|
12247
|
+
log4(`[seal-mcp] recovery: left ${handle} alone \u2014 pid ${op.owner.pid} on ${op.owner.hostname} may still run it`);
|
|
12248
|
+
continue;
|
|
12249
|
+
}
|
|
12207
12250
|
if (op.seal_id === void 0) {
|
|
12208
12251
|
delete view.pending[handle];
|
|
12209
12252
|
report.dropped.push(handle);
|
|
@@ -12293,7 +12336,7 @@ var AES_KEY_BYTES = 32;
|
|
|
12293
12336
|
var AES_NONCE_BYTES = 12;
|
|
12294
12337
|
var StatePassphraseInvalid = class extends Error {
|
|
12295
12338
|
constructor() {
|
|
12296
|
-
super("
|
|
12339
|
+
super("AES-256-GCM tag check failed: another passphrase, or a tampered file");
|
|
12297
12340
|
this.name = "StatePassphraseInvalid";
|
|
12298
12341
|
}
|
|
12299
12342
|
};
|
|
@@ -12493,6 +12536,13 @@ async function saveState(state, passphrase, statePath) {
|
|
|
12493
12536
|
await import_node_fs2.promises.chmod(dir, 448).catch(() => {
|
|
12494
12537
|
});
|
|
12495
12538
|
}
|
|
12539
|
+
function updateState(passphrase, statePath, mutate) {
|
|
12540
|
+
return withStateLock((0, import_node_path2.dirname)(statePath), async () => {
|
|
12541
|
+
const view = await loadState(passphrase, statePath);
|
|
12542
|
+
if (await mutate(view) !== false) await saveState(view, passphrase, statePath);
|
|
12543
|
+
return view;
|
|
12544
|
+
});
|
|
12545
|
+
}
|
|
12496
12546
|
|
|
12497
12547
|
// src/tools/context.ts
|
|
12498
12548
|
var import_node_crypto3 = require("crypto");
|
|
@@ -12510,19 +12560,18 @@ var HANDLE_REGEX = /^h_[A-Za-z0-9_-]{12,}$/;
|
|
|
12510
12560
|
function isValidHandle(handle) {
|
|
12511
12561
|
return typeof handle === "string" && HANDLE_REGEX.test(handle);
|
|
12512
12562
|
}
|
|
12563
|
+
function auditEntry(ctx, tool, handle, mode) {
|
|
12564
|
+
return { ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(), tool, handle, mode };
|
|
12565
|
+
}
|
|
12513
12566
|
function appendAudit(ctx, tool, handle, mode) {
|
|
12514
|
-
ctx
|
|
12515
|
-
|
|
12516
|
-
tool,
|
|
12517
|
-
handle,
|
|
12518
|
-
mode
|
|
12519
|
-
});
|
|
12567
|
+
const entry = auditEntry(ctx, tool, handle, mode);
|
|
12568
|
+
return ctx.update((state) => void state.audit.push(entry));
|
|
12520
12569
|
}
|
|
12521
|
-
function insertHandle(
|
|
12522
|
-
if (
|
|
12570
|
+
function insertHandle(state, handle, entry) {
|
|
12571
|
+
if (state.handles[handle]) {
|
|
12523
12572
|
throw new Error(`insertHandle: collision on ${handle}`);
|
|
12524
12573
|
}
|
|
12525
|
-
|
|
12574
|
+
state.handles[handle] = entry;
|
|
12526
12575
|
}
|
|
12527
12576
|
function lookupHandle(ctx, handle) {
|
|
12528
12577
|
const entry = ctx.state.handles[handle];
|
|
@@ -12545,6 +12594,7 @@ var ToolError = class extends Error {
|
|
|
12545
12594
|
// src/tools/list.ts
|
|
12546
12595
|
var SINGLE_FILE_COUNT = 1;
|
|
12547
12596
|
async function sealList(ctx, input = {}) {
|
|
12597
|
+
await appendAudit(ctx, "seal_list", null, null);
|
|
12548
12598
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
12549
12599
|
const entries = [];
|
|
12550
12600
|
for (const [handle, entry] of Object.entries(ctx.state.handles)) {
|
|
@@ -12561,8 +12611,6 @@ async function sealList(ctx, input = {}) {
|
|
|
12561
12611
|
});
|
|
12562
12612
|
}
|
|
12563
12613
|
entries.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
12564
|
-
appendAudit(ctx, "seal_list", null, null);
|
|
12565
|
-
await ctx.persist();
|
|
12566
12614
|
return { entries };
|
|
12567
12615
|
}
|
|
12568
12616
|
|
|
@@ -12647,13 +12695,11 @@ async function sealOpen(ctx, input) {
|
|
|
12647
12695
|
if (mode === "inline") {
|
|
12648
12696
|
const refusal = classifyInlineRefusal(first.mime);
|
|
12649
12697
|
if (refusal !== null) {
|
|
12650
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12651
|
-
await ctx.persist();
|
|
12698
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12652
12699
|
return { handle, refusal, mime: first.mime, note: refusalNote(refusal, first.mime) };
|
|
12653
12700
|
}
|
|
12654
12701
|
if (first.size > INLINE_CIPHERTEXT_MAX_BYTES) {
|
|
12655
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12656
|
-
await ctx.persist();
|
|
12702
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12657
12703
|
return {
|
|
12658
12704
|
handle,
|
|
12659
12705
|
refusal: "inline_blocked.too_large",
|
|
@@ -12689,12 +12735,10 @@ async function sealOpen(ctx, input) {
|
|
|
12689
12735
|
err2
|
|
12690
12736
|
);
|
|
12691
12737
|
}
|
|
12692
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12693
|
-
await ctx.persist();
|
|
12738
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12694
12739
|
return { handle, mime: bareMime(first.mime), sizeBytes: plaintext.byteLength, encoding: "utf-8", content };
|
|
12695
12740
|
}
|
|
12696
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12697
|
-
await ctx.persist();
|
|
12741
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12698
12742
|
return {
|
|
12699
12743
|
handle,
|
|
12700
12744
|
filename: first.name,
|
|
@@ -12740,8 +12784,7 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12740
12784
|
throw streamError(err2, file.id);
|
|
12741
12785
|
}
|
|
12742
12786
|
}
|
|
12743
|
-
appendAudit(ctx, "seal_open", handle, "file");
|
|
12744
|
-
await ctx.persist();
|
|
12787
|
+
await appendAudit(ctx, "seal_open", handle, "file");
|
|
12745
12788
|
return { handle, files: out };
|
|
12746
12789
|
}
|
|
12747
12790
|
|
|
@@ -12844,8 +12887,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
12844
12887
|
if (input.whereUrl) cliArgs.push("--where", input.whereUrl);
|
|
12845
12888
|
const local = await (deps.runCli ?? runSealCli)(cliArgs);
|
|
12846
12889
|
if (local && "files" in local) {
|
|
12847
|
-
appendAudit(ctx, "seal_request", handle, "local");
|
|
12848
|
-
await ctx.persist();
|
|
12890
|
+
await appendAudit(ctx, "seal_request", handle, "local");
|
|
12849
12891
|
return {
|
|
12850
12892
|
handle,
|
|
12851
12893
|
source: "local",
|
|
@@ -12885,8 +12927,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
12885
12927
|
}
|
|
12886
12928
|
return receiveThroughLink(ctx, handle, intake.id, deps, input.onProgress);
|
|
12887
12929
|
}
|
|
12888
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
12889
|
-
await ctx.persist();
|
|
12930
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
12890
12931
|
return { handle, url, emoji, expiresAt: intake.expires_at };
|
|
12891
12932
|
}
|
|
12892
12933
|
async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
@@ -12907,8 +12948,7 @@ async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
|
12907
12948
|
const opened = await sealOpen(ctx, { url: `${deps.publicHost}${sharePath}`, mode: "file", dir, onProgress });
|
|
12908
12949
|
if (!("files" in opened)) throw new ToolError("internal_inconsistency", "seal_open mode=file returned no files");
|
|
12909
12950
|
ctx.requests?.delete(handle);
|
|
12910
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
12911
|
-
await ctx.persist();
|
|
12951
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
12912
12952
|
return {
|
|
12913
12953
|
handle,
|
|
12914
12954
|
source: "link",
|
|
@@ -12921,6 +12961,7 @@ async function sealRevoke(ctx, input) {
|
|
|
12921
12961
|
if (!isValidHandle(input.handle)) {
|
|
12922
12962
|
throw new ToolError("handle_invalid", `handle does not match contract pattern: ${input.handle}`);
|
|
12923
12963
|
}
|
|
12964
|
+
await ctx.update(() => false);
|
|
12924
12965
|
const entry = lookupHandle(ctx, input.handle);
|
|
12925
12966
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
12926
12967
|
if (entry.revoked_at) {
|
|
@@ -12955,15 +12996,17 @@ async function sealRevoke(ctx, input) {
|
|
|
12955
12996
|
err2
|
|
12956
12997
|
);
|
|
12957
12998
|
}
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12999
|
+
await ctx.update((state) => {
|
|
13000
|
+
state.handles[input.handle] = { ...entry, revoked_at: now };
|
|
13001
|
+
state.audit.push(auditEntry(ctx, "seal_revoke", input.handle, entry.mode));
|
|
13002
|
+
});
|
|
12961
13003
|
return { revoked: true, revokedAt: now };
|
|
12962
13004
|
}
|
|
12963
13005
|
|
|
12964
13006
|
// src/tools/share.ts
|
|
12965
13007
|
var import_node_fs5 = require("fs");
|
|
12966
13008
|
var import_promises9 = require("fs/promises");
|
|
13009
|
+
var import_node_os7 = require("os");
|
|
12967
13010
|
var import_node_path8 = require("path");
|
|
12968
13011
|
|
|
12969
13012
|
// ../../contracts/limits.json
|
|
@@ -13190,6 +13233,7 @@ function guessMimeFromName(name) {
|
|
|
13190
13233
|
}
|
|
13191
13234
|
var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
|
|
13192
13235
|
var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
|
|
13236
|
+
var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
|
|
13193
13237
|
var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
|
|
13194
13238
|
var PAYMENT_RECEIPT = "The file is larger than 1 GB. Show the user the payment link; the upload continues after payment. Then call seal_share with `payment` set to this handle.";
|
|
13195
13239
|
function defaultExpire(mode, size) {
|
|
@@ -13217,31 +13261,46 @@ var FORWARD_MAX_READS_FORCED = 1;
|
|
|
13217
13261
|
function nowIso2(ctx) {
|
|
13218
13262
|
return (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13219
13263
|
}
|
|
13220
|
-
|
|
13264
|
+
function journal(ctx, handle, op) {
|
|
13265
|
+
return ctx.update((state) => {
|
|
13266
|
+
state.pending[handle] = op;
|
|
13267
|
+
});
|
|
13268
|
+
}
|
|
13269
|
+
function dropJournal(ctx, handle) {
|
|
13270
|
+
return ctx.update((state) => {
|
|
13271
|
+
delete state.pending[handle];
|
|
13272
|
+
});
|
|
13273
|
+
}
|
|
13274
|
+
function complete(ctx, handle, entry) {
|
|
13275
|
+
return ctx.update((state) => {
|
|
13276
|
+
insertHandle(state, handle, entry);
|
|
13277
|
+
state.audit.push(auditEntry(ctx, "seal_share", handle, entry.mode));
|
|
13278
|
+
delete state.pending[handle];
|
|
13279
|
+
});
|
|
13280
|
+
}
|
|
13281
|
+
async function bestEffort(write) {
|
|
13221
13282
|
try {
|
|
13222
|
-
await
|
|
13283
|
+
await write;
|
|
13223
13284
|
} catch (err2) {
|
|
13224
13285
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13225
|
-
process.stderr.write(`[seal-mcp] warn: journal
|
|
13286
|
+
process.stderr.write(`[seal-mcp] warn: journal write failed: ${msg}
|
|
13226
13287
|
`);
|
|
13227
13288
|
}
|
|
13228
13289
|
}
|
|
13229
|
-
async function compensateRevoke(ctx, handle) {
|
|
13230
|
-
const
|
|
13231
|
-
|
|
13232
|
-
if (!op || sealId === void 0) {
|
|
13290
|
+
async function compensateRevoke(ctx, handle, op) {
|
|
13291
|
+
const sealId = op.seal_id;
|
|
13292
|
+
if (sealId === void 0) {
|
|
13233
13293
|
return "no live seal to revoke";
|
|
13234
13294
|
}
|
|
13235
13295
|
try {
|
|
13236
13296
|
await ctx.client.revoke(sealId, op.owner_token_b64);
|
|
13237
|
-
|
|
13238
|
-
await persistBestEffort(ctx);
|
|
13297
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13239
13298
|
return "seal auto-revoked";
|
|
13240
13299
|
} catch (err2) {
|
|
13241
13300
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13242
13301
|
op.step = "compensating";
|
|
13243
13302
|
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso2(ctx) };
|
|
13244
|
-
await
|
|
13303
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13245
13304
|
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`;
|
|
13246
13305
|
}
|
|
13247
13306
|
}
|
|
@@ -13413,13 +13472,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13413
13472
|
// backend this seal is actually created on — recovery refuses to
|
|
13414
13473
|
// "revoke" against a different backend whose 404 would be misread
|
|
13415
13474
|
// as success.
|
|
13416
|
-
backend_url: ctx.client.baseUrl
|
|
13475
|
+
backend_url: ctx.client.baseUrl,
|
|
13476
|
+
// SPEC-ADDRESS F1 §4: another MCP host starting now must not recover
|
|
13477
|
+
// (revoke) this upload while it runs.
|
|
13478
|
+
owner: { pid: process.pid, hostname: (0, import_node_os7.hostname)() }
|
|
13417
13479
|
};
|
|
13418
|
-
ctx.state.pending[handle] = op;
|
|
13419
13480
|
try {
|
|
13420
|
-
await ctx
|
|
13481
|
+
await journal(ctx, handle, op);
|
|
13421
13482
|
} catch (err2) {
|
|
13422
|
-
delete ctx.state.pending[handle];
|
|
13423
13483
|
throw new ToolError(
|
|
13424
13484
|
"state_persist_failed",
|
|
13425
13485
|
`refusing to upload: could not journal the operation before its first remote effect: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13440,7 +13500,7 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13440
13500
|
// `uploaded` checkpoint after this call is the durability gate.
|
|
13441
13501
|
onSealCreated: async (sealId) => {
|
|
13442
13502
|
op.seal_id = sealId;
|
|
13443
|
-
await
|
|
13503
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13444
13504
|
}
|
|
13445
13505
|
});
|
|
13446
13506
|
} catch (err2) {
|
|
@@ -13449,15 +13509,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13449
13509
|
return awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, expiresAt, session });
|
|
13450
13510
|
}
|
|
13451
13511
|
if (op.seal_id !== void 0) {
|
|
13452
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13512
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13453
13513
|
throw new ToolError(
|
|
13454
13514
|
"upload_failed",
|
|
13455
13515
|
`seal upload failed after the seal was created: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13456
13516
|
err2
|
|
13457
13517
|
);
|
|
13458
13518
|
}
|
|
13459
|
-
|
|
13460
|
-
await persistBestEffort(ctx);
|
|
13519
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13461
13520
|
throw new ToolError(
|
|
13462
13521
|
"upload_failed",
|
|
13463
13522
|
`seal upload failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13470,9 +13529,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13470
13529
|
op.seal_id = uploaded.sealId;
|
|
13471
13530
|
op.step = "uploaded";
|
|
13472
13531
|
try {
|
|
13473
|
-
await ctx
|
|
13532
|
+
await journal(ctx, handle, op);
|
|
13474
13533
|
} catch (err2) {
|
|
13475
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13534
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13476
13535
|
throw new ToolError(
|
|
13477
13536
|
"state_persist_failed",
|
|
13478
13537
|
`could not journal the live seal after upload: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13485,7 +13544,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13485
13544
|
try {
|
|
13486
13545
|
shareBlobBytes = await encryptShareBlob(fileKeysB64, shareLinkKey, void 0);
|
|
13487
13546
|
} catch (err2) {
|
|
13488
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13547
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13489
13548
|
throw new ToolError(
|
|
13490
13549
|
"share_blob_failed",
|
|
13491
13550
|
`share blob encryption failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13497,7 +13556,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13497
13556
|
try {
|
|
13498
13557
|
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13499
13558
|
} catch (err2) {
|
|
13500
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13559
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13501
13560
|
throw new ToolError(
|
|
13502
13561
|
"share_link_failed",
|
|
13503
13562
|
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13515,7 +13574,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13515
13574
|
try {
|
|
13516
13575
|
fragment = await buildTargetedFragment(shareLinkKey, recipientPub, uploaded.sealId);
|
|
13517
13576
|
} catch (err2) {
|
|
13518
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13577
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13519
13578
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
13520
13579
|
if (message.includes("non-contributory")) {
|
|
13521
13580
|
throw new ToolError(
|
|
@@ -13536,7 +13595,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13536
13595
|
const host = resolvePublicHost(deps.publicHost);
|
|
13537
13596
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13538
13597
|
if (mode === "forward") {
|
|
13539
|
-
|
|
13598
|
+
await complete(ctx, handle, {
|
|
13540
13599
|
seal_id: uploaded.sealId,
|
|
13541
13600
|
owner_token_b64: ownerTokenB64,
|
|
13542
13601
|
created_at: nowIso2(ctx),
|
|
@@ -13544,9 +13603,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13544
13603
|
expires_at: expiresAt,
|
|
13545
13604
|
backend_url: ctx.client.baseUrl
|
|
13546
13605
|
});
|
|
13547
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13548
|
-
delete ctx.state.pending[handle];
|
|
13549
|
-
await ctx.persist();
|
|
13550
13606
|
return {
|
|
13551
13607
|
handle,
|
|
13552
13608
|
shareUrl,
|
|
@@ -13558,9 +13614,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13558
13614
|
op.step = "delivering";
|
|
13559
13615
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13560
13616
|
try {
|
|
13561
|
-
await ctx
|
|
13617
|
+
await journal(ctx, handle, op);
|
|
13562
13618
|
} catch (err2) {
|
|
13563
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13619
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13564
13620
|
throw new ToolError(
|
|
13565
13621
|
"state_persist_failed",
|
|
13566
13622
|
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13571,14 +13627,14 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13571
13627
|
try {
|
|
13572
13628
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
13573
13629
|
} catch (err2) {
|
|
13574
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13630
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13575
13631
|
throw new ToolError(
|
|
13576
13632
|
"handoff_both_channels_failed",
|
|
13577
13633
|
`both clipboard and file handoff channels failed \u2014 ${outcome}`,
|
|
13578
13634
|
err2
|
|
13579
13635
|
);
|
|
13580
13636
|
}
|
|
13581
|
-
|
|
13637
|
+
await complete(ctx, handle, {
|
|
13582
13638
|
seal_id: uploaded.sealId,
|
|
13583
13639
|
owner_token_b64: ownerTokenB64,
|
|
13584
13640
|
created_at: nowIso2(ctx),
|
|
@@ -13587,10 +13643,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13587
13643
|
handoff_channel: handoff.channel,
|
|
13588
13644
|
backend_url: ctx.client.baseUrl
|
|
13589
13645
|
});
|
|
13590
|
-
|
|
13591
|
-
delete ctx.state.pending[handle];
|
|
13592
|
-
await ctx.persist();
|
|
13593
|
-
const delivered = handoff.channel === "clipboard" ? "The seal link is on your clipboard. Paste it to the recipient." : `The seal link is in ${handoff.file} (mode 0600, removed automatically later). Give it to the recipient.`;
|
|
13646
|
+
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13594
13647
|
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.`;
|
|
13595
13648
|
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;
|
|
13596
13649
|
return {
|
|
@@ -13608,12 +13661,12 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13608
13661
|
const tier = prepared.tier;
|
|
13609
13662
|
const sealId = op.seal_id;
|
|
13610
13663
|
op.step = "awaiting_payment";
|
|
13611
|
-
await
|
|
13664
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13612
13665
|
let paymentUrl;
|
|
13613
13666
|
try {
|
|
13614
13667
|
paymentUrl = await ctx.client.createCheckout(sealId, ownerTokenB64, tier);
|
|
13615
13668
|
} catch (err2) {
|
|
13616
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13669
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13617
13670
|
throw new ToolError("payment_failed", `POST /billing/checkout failed: ${messageOf3(err2)} (${outcome})`, err2);
|
|
13618
13671
|
}
|
|
13619
13672
|
const deadline = paymentDeadline(op.expires_at);
|
|
@@ -13623,7 +13676,7 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13623
13676
|
clearTimeout(timer);
|
|
13624
13677
|
ctx.payments?.delete(handle);
|
|
13625
13678
|
await prepared.cleanup();
|
|
13626
|
-
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle)})`);
|
|
13679
|
+
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle, op)})`);
|
|
13627
13680
|
};
|
|
13628
13681
|
const finish = async (onProgress) => {
|
|
13629
13682
|
if (running) throw new ToolError("payment_invalid", `share ${handle} is already being finished`);
|
|
@@ -13792,7 +13845,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13792
13845
|
}
|
|
13793
13846
|
|
|
13794
13847
|
// src/server.ts
|
|
13795
|
-
var SERVER_VERSION = "0.
|
|
13848
|
+
var SERVER_VERSION = "0.3.0";
|
|
13796
13849
|
var toolMeta = (name) => {
|
|
13797
13850
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13798
13851
|
return { title, description, annotations };
|
|
@@ -13819,41 +13872,29 @@ function progressReporter(extra) {
|
|
|
13819
13872
|
}
|
|
13820
13873
|
async function createServer(options) {
|
|
13821
13874
|
logEngineToStderr();
|
|
13822
|
-
let view;
|
|
13823
|
-
let identity;
|
|
13824
|
-
let lock = null;
|
|
13825
13875
|
const client = new SealApi({ baseUrl: resolveBackendUrl(options.backendBaseUrl) });
|
|
13826
13876
|
const disk = options.state;
|
|
13877
|
+
let view;
|
|
13878
|
+
const update = async (mutate) => {
|
|
13879
|
+
if (disk === null) await mutate(view);
|
|
13880
|
+
else view = await updateState(disk.passphrase, disk.path, mutate);
|
|
13881
|
+
};
|
|
13882
|
+
let identity;
|
|
13827
13883
|
if (disk === null) {
|
|
13828
13884
|
identity = await generateIdentity();
|
|
13829
13885
|
view = createEmptyState(identityToFields(identity));
|
|
13830
13886
|
} else {
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
try {
|
|
13834
|
-
view = await loadState(disk.passphrase, disk.path);
|
|
13835
|
-
identity = await loadIdentity(view.identity);
|
|
13836
|
-
const { changed } = await recoverPendingOperations(view, client);
|
|
13837
|
-
if (changed) {
|
|
13838
|
-
await saveState(view, disk.passphrase, disk.path);
|
|
13839
|
-
}
|
|
13840
|
-
} catch (err2) {
|
|
13841
|
-
await held.release();
|
|
13842
|
-
throw err2;
|
|
13843
|
-
}
|
|
13887
|
+
view = await updateState(disk.passphrase, disk.path, async (state) => (await recoverPendingOperations(state, client)).changed);
|
|
13888
|
+
identity = await loadIdentity(view.identity);
|
|
13844
13889
|
}
|
|
13845
13890
|
const publicHost = resolvePublicHost(options.publicHost);
|
|
13846
|
-
let persistChain = Promise.resolve();
|
|
13847
|
-
const persist = () => {
|
|
13848
|
-
if (disk === null) return Promise.resolve();
|
|
13849
|
-
persistChain = persistChain.catch(() => void 0).then(() => saveState(view, disk.passphrase, disk.path));
|
|
13850
|
-
return persistChain;
|
|
13851
|
-
};
|
|
13852
13891
|
const ctx = {
|
|
13853
|
-
state
|
|
13892
|
+
get state() {
|
|
13893
|
+
return view;
|
|
13894
|
+
},
|
|
13854
13895
|
identity,
|
|
13855
13896
|
client,
|
|
13856
|
-
|
|
13897
|
+
update,
|
|
13857
13898
|
ephemeral: disk === null,
|
|
13858
13899
|
requests: /* @__PURE__ */ new Map(),
|
|
13859
13900
|
payments: /* @__PURE__ */ new Map()
|
|
@@ -14056,18 +14097,12 @@ async function createServer(options) {
|
|
|
14056
14097
|
);
|
|
14057
14098
|
}
|
|
14058
14099
|
const transport = new import_stdio.StdioServerTransport();
|
|
14059
|
-
|
|
14060
|
-
await mcp.connect(transport);
|
|
14061
|
-
} catch (err2) {
|
|
14062
|
-
await lock?.release();
|
|
14063
|
-
throw err2;
|
|
14064
|
-
}
|
|
14100
|
+
await mcp.connect(transport);
|
|
14065
14101
|
const close = async () => {
|
|
14066
14102
|
try {
|
|
14067
14103
|
await mcp.close();
|
|
14068
14104
|
} finally {
|
|
14069
14105
|
await transport.close().catch(() => void 0);
|
|
14070
|
-
await lock?.release();
|
|
14071
14106
|
}
|
|
14072
14107
|
};
|
|
14073
14108
|
return { mcp, transport, close };
|
|
@@ -14081,7 +14116,7 @@ async function wrapTool(fn, toWire) {
|
|
|
14081
14116
|
structuredContent: wire
|
|
14082
14117
|
};
|
|
14083
14118
|
} catch (err2) {
|
|
14084
|
-
if (err2 instanceof ToolError) {
|
|
14119
|
+
if (err2 instanceof ToolError || err2 instanceof StateBusyError) {
|
|
14085
14120
|
return {
|
|
14086
14121
|
isError: true,
|
|
14087
14122
|
content: [
|
|
@@ -14101,8 +14136,8 @@ async function wrapTool(fn, toWire) {
|
|
|
14101
14136
|
}
|
|
14102
14137
|
|
|
14103
14138
|
// src/paths.ts
|
|
14104
|
-
var
|
|
14105
|
-
var
|
|
14139
|
+
var import_node_os8 = require("os");
|
|
14140
|
+
var import_node_path9 = require("path");
|
|
14106
14141
|
var APP_NAME = "seal-mcp";
|
|
14107
14142
|
var STATE_FILE_NAME = "state.json";
|
|
14108
14143
|
function resolveStateDir(opts = {}) {
|
|
@@ -14110,7 +14145,7 @@ function resolveStateDir(opts = {}) {
|
|
|
14110
14145
|
if (opts.cliFlag !== void 0 && opts.cliFlag.length > 0) {
|
|
14111
14146
|
return {
|
|
14112
14147
|
dir: opts.cliFlag,
|
|
14113
|
-
statePath: (0,
|
|
14148
|
+
statePath: (0, import_node_path9.join)(opts.cliFlag, STATE_FILE_NAME),
|
|
14114
14149
|
source: "flag"
|
|
14115
14150
|
};
|
|
14116
14151
|
}
|
|
@@ -14118,51 +14153,56 @@ function resolveStateDir(opts = {}) {
|
|
|
14118
14153
|
if (envOverride !== void 0 && envOverride.length > 0) {
|
|
14119
14154
|
return {
|
|
14120
14155
|
dir: envOverride,
|
|
14121
|
-
statePath: (0,
|
|
14156
|
+
statePath: (0, import_node_path9.join)(envOverride, STATE_FILE_NAME),
|
|
14122
14157
|
source: "env"
|
|
14123
14158
|
};
|
|
14124
14159
|
}
|
|
14125
|
-
const dir = osDefaultDir(env, opts.platform ?? process.platform, opts.home ?? (0,
|
|
14126
|
-
return { dir, statePath: (0,
|
|
14160
|
+
const dir = osDefaultDir(env, opts.platform ?? process.platform, opts.home ?? (0, import_node_os8.homedir)());
|
|
14161
|
+
return { dir, statePath: (0, import_node_path9.join)(dir, STATE_FILE_NAME), source: "os-default" };
|
|
14127
14162
|
}
|
|
14128
14163
|
function osDefaultDir(env, platform, home) {
|
|
14129
14164
|
switch (platform) {
|
|
14130
14165
|
case "darwin":
|
|
14131
|
-
return (0,
|
|
14166
|
+
return (0, import_node_path9.join)(home, "Library", "Application Support", APP_NAME);
|
|
14132
14167
|
case "win32": {
|
|
14133
14168
|
const appdata = env.APPDATA;
|
|
14134
|
-
return appdata !== void 0 && appdata.length > 0 ? (0,
|
|
14169
|
+
return appdata !== void 0 && appdata.length > 0 ? (0, import_node_path9.join)(appdata, APP_NAME) : (0, import_node_path9.join)(home, "AppData", "Roaming", APP_NAME);
|
|
14135
14170
|
}
|
|
14136
14171
|
default: {
|
|
14137
14172
|
const xdg = env.XDG_CONFIG_HOME;
|
|
14138
|
-
return xdg !== void 0 && xdg.length > 0 ? (0,
|
|
14173
|
+
return xdg !== void 0 && xdg.length > 0 ? (0, import_node_path9.join)(xdg, APP_NAME) : (0, import_node_path9.join)(home, ".config", APP_NAME);
|
|
14139
14174
|
}
|
|
14140
14175
|
}
|
|
14141
14176
|
}
|
|
14142
14177
|
|
|
14143
|
-
// src/
|
|
14178
|
+
// src/seed.ts
|
|
14144
14179
|
var import_node_child_process2 = require("child_process");
|
|
14145
14180
|
var import_node_crypto4 = require("crypto");
|
|
14146
|
-
var
|
|
14147
|
-
var
|
|
14148
|
-
var
|
|
14149
|
-
var
|
|
14150
|
-
var
|
|
14151
|
-
var
|
|
14152
|
-
var
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14181
|
+
var import_node_fs6 = require("fs");
|
|
14182
|
+
var import_node_path10 = require("path");
|
|
14183
|
+
var SEED_ENV_VAR = "SEAL_SEED";
|
|
14184
|
+
var EPHEMERAL_ENV_VAR = "SEAL_MCP_EPHEMERAL";
|
|
14185
|
+
var PASSPHRASE_ENV_VAR = "SEAL_MCP_PASSPHRASE";
|
|
14186
|
+
var SEED_FILE_NAME = "seed";
|
|
14187
|
+
var KEYCHAIN_SERVICE = "seal-mcp";
|
|
14188
|
+
var KEYCHAIN_ACCOUNT = "seed";
|
|
14189
|
+
var LEGACY_KEYCHAIN_ACCOUNT = "state";
|
|
14190
|
+
var SeedInvalidError = class extends Error {
|
|
14191
|
+
code = "seed_invalid";
|
|
14192
|
+
constructor(where) {
|
|
14193
|
+
super(
|
|
14194
|
+
`${where} is not an agent key: expected 43 base64url characters, as \`sealnet-mcp seed --force\` or \`seal keygen --export --force\` print them`
|
|
14195
|
+
);
|
|
14196
|
+
this.name = "SeedInvalidError";
|
|
14197
|
+
}
|
|
14198
|
+
};
|
|
14199
|
+
var IdentityConflictError = class extends Error {
|
|
14200
|
+
code = "identity_conflict";
|
|
14201
|
+
constructor(statePath) {
|
|
14162
14202
|
super(
|
|
14163
|
-
|
|
14203
|
+
`${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.`
|
|
14164
14204
|
);
|
|
14165
|
-
this.name = "
|
|
14205
|
+
this.name = "IdentityConflictError";
|
|
14166
14206
|
}
|
|
14167
14207
|
};
|
|
14168
14208
|
function keyringEntry(keyring, platform, service, account) {
|
|
@@ -14197,114 +14237,147 @@ async function loadKeytar() {
|
|
|
14197
14237
|
try {
|
|
14198
14238
|
const mod = await import("@napi-rs/keyring");
|
|
14199
14239
|
const keyring = mod.default ?? mod;
|
|
14200
|
-
keyringEntry(keyring, process.platform,
|
|
14240
|
+
keyringEntry(keyring, process.platform, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
14201
14241
|
keytarCache = keytarOverKeyring(keyring);
|
|
14202
14242
|
} catch {
|
|
14203
14243
|
keytarCache = null;
|
|
14204
14244
|
}
|
|
14205
14245
|
return keytarCache;
|
|
14206
14246
|
}
|
|
14207
|
-
async function
|
|
14208
|
-
const
|
|
14209
|
-
if (
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14247
|
+
async function keychainGet(account) {
|
|
14248
|
+
const keytar = await loadKeytar();
|
|
14249
|
+
if (keytar === null) return null;
|
|
14250
|
+
try {
|
|
14251
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, account);
|
|
14252
|
+
return value !== null && value.length > 0 ? value : null;
|
|
14253
|
+
} catch {
|
|
14254
|
+
return null;
|
|
14213
14255
|
}
|
|
14214
|
-
return k;
|
|
14215
14256
|
}
|
|
14216
|
-
async function
|
|
14257
|
+
async function keychainDelete(account) {
|
|
14258
|
+
const keytar = await loadKeytar();
|
|
14259
|
+
if (keytar === null) return false;
|
|
14260
|
+
try {
|
|
14261
|
+
return await keytar.deletePassword(KEYCHAIN_SERVICE, account);
|
|
14262
|
+
} catch {
|
|
14263
|
+
return false;
|
|
14264
|
+
}
|
|
14265
|
+
}
|
|
14266
|
+
function parseSeed(text, where) {
|
|
14267
|
+
const seed = Buffer.from(text, "base64url");
|
|
14268
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(text) || seed.toString("base64url") !== text) throw new SeedInvalidError(where);
|
|
14269
|
+
return new Uint8Array(seed);
|
|
14270
|
+
}
|
|
14271
|
+
async function readSeed(stateDir, env = process.env) {
|
|
14272
|
+
const fromEnv = env[SEED_ENV_VAR]?.trim();
|
|
14273
|
+
if (fromEnv) return { seed: parseSeed(fromEnv, SEED_ENV_VAR), source: "env" };
|
|
14274
|
+
const stored = await keychainGet(KEYCHAIN_ACCOUNT);
|
|
14275
|
+
if (stored !== null) return { seed: parseSeed(stored, `keychain entry ${KEYCHAIN_SERVICE}/${KEYCHAIN_ACCOUNT}`), source: "keychain" };
|
|
14276
|
+
const file = (0, import_node_path10.join)(stateDir, SEED_FILE_NAME);
|
|
14277
|
+
let text;
|
|
14278
|
+
try {
|
|
14279
|
+
text = await import_node_fs6.promises.readFile(file, "utf8");
|
|
14280
|
+
} catch (err2) {
|
|
14281
|
+
if (err2.code === "ENOENT") return null;
|
|
14282
|
+
throw err2;
|
|
14283
|
+
}
|
|
14284
|
+
return { seed: parseSeed(text.trim(), file), source: "file" };
|
|
14285
|
+
}
|
|
14286
|
+
async function storeSeed(stateDir, seed) {
|
|
14287
|
+
const text = Buffer.from(seed).toString("base64url");
|
|
14217
14288
|
const keytar = await loadKeytar();
|
|
14218
14289
|
if (keytar !== null) {
|
|
14219
14290
|
try {
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
return { passphrase: stored, source: "keychain" };
|
|
14223
|
-
}
|
|
14291
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, text);
|
|
14292
|
+
return "keychain";
|
|
14224
14293
|
} catch {
|
|
14225
14294
|
}
|
|
14226
14295
|
}
|
|
14227
|
-
const
|
|
14228
|
-
|
|
14229
|
-
|
|
14296
|
+
const fh = await import_node_fs6.promises.open((0, import_node_path10.join)(stateDir, SEED_FILE_NAME), "wx", 384);
|
|
14297
|
+
try {
|
|
14298
|
+
await fh.writeFile(`${text}
|
|
14299
|
+
`, "utf8");
|
|
14300
|
+
await fh.sync();
|
|
14301
|
+
} finally {
|
|
14302
|
+
await fh.close();
|
|
14230
14303
|
}
|
|
14231
|
-
return
|
|
14304
|
+
return "file";
|
|
14232
14305
|
}
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14306
|
+
function statePassphrase(seed) {
|
|
14307
|
+
return Buffer.from((0, import_node_crypto4.hkdfSync)("sha256", seed, "seal-mcp:state:v1", "passphrase", 32)).toString("base64url");
|
|
14308
|
+
}
|
|
14309
|
+
async function legacyPassphrase(env) {
|
|
14310
|
+
const stored = await keychainGet(LEGACY_KEYCHAIN_ACCOUNT);
|
|
14311
|
+
if (stored !== null) return { passphrase: stored, source: "keychain" };
|
|
14312
|
+
const fromEnv = env[PASSPHRASE_ENV_VAR];
|
|
14313
|
+
return fromEnv ? { passphrase: fromEnv, source: "env" } : null;
|
|
14314
|
+
}
|
|
14315
|
+
async function tryOpen(passphrase, statePath) {
|
|
14236
14316
|
try {
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14317
|
+
return await loadState(passphrase, statePath);
|
|
14318
|
+
} catch (err2) {
|
|
14319
|
+
if (err2 instanceof StatePassphraseInvalid) return null;
|
|
14320
|
+
throw err2;
|
|
14241
14321
|
}
|
|
14242
14322
|
}
|
|
14243
|
-
async function
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14323
|
+
async function requireIdentity(view, seed, statePath) {
|
|
14324
|
+
const { secret } = await loadIdentity(view.identity);
|
|
14325
|
+
if (!Buffer.from(secret).equals(Buffer.from(seed))) throw new IdentityConflictError(statePath);
|
|
14326
|
+
}
|
|
14327
|
+
function openAgentState(path, env = process.env) {
|
|
14328
|
+
return withStateLock(path.dir, async () => {
|
|
14329
|
+
let root = await readSeed(path.dir, env);
|
|
14330
|
+
if (!await import_node_fs6.promises.stat(path.statePath).then(() => true, () => false)) {
|
|
14331
|
+
if (root === null) {
|
|
14332
|
+
const { secret } = await generateIdentity();
|
|
14333
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
14334
|
+
}
|
|
14335
|
+
const passphrase2 = statePassphrase(root.seed);
|
|
14336
|
+
const identity = await identityFromSeed(root.seed);
|
|
14337
|
+
await saveState(createEmptyState(identityToFields(identity)), passphrase2, path.statePath);
|
|
14338
|
+
return { root, passphrase: passphrase2, created: true, moved: null };
|
|
14339
|
+
}
|
|
14340
|
+
if (root !== null) {
|
|
14341
|
+
const view2 = await tryOpen(statePassphrase(root.seed), path.statePath);
|
|
14342
|
+
if (view2 !== null) {
|
|
14343
|
+
await requireIdentity(view2, root.seed, path.statePath);
|
|
14344
|
+
return { root, passphrase: statePassphrase(root.seed), created: false, moved: null };
|
|
14345
|
+
}
|
|
14346
|
+
}
|
|
14347
|
+
const legacy = await legacyPassphrase(env);
|
|
14348
|
+
const view = legacy === null ? null : await tryOpen(legacy.passphrase, path.statePath);
|
|
14349
|
+
if (legacy === null || view === null) throw new IdentityConflictError(path.statePath);
|
|
14350
|
+
if (root !== null) await requireIdentity(view, root.seed, path.statePath);
|
|
14351
|
+
else {
|
|
14352
|
+
const { secret } = await loadIdentity(view.identity);
|
|
14353
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
14354
|
+
}
|
|
14355
|
+
const passphrase = statePassphrase(root.seed);
|
|
14356
|
+
await saveState(view, passphrase, path.statePath);
|
|
14357
|
+
await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
14358
|
+
return { root, passphrase, created: false, moved: legacy.source };
|
|
14359
|
+
});
|
|
14249
14360
|
}
|
|
14250
|
-
async function
|
|
14361
|
+
async function deleteRootSecret(stateDir) {
|
|
14362
|
+
let seedFile = true;
|
|
14363
|
+
await import_node_fs6.promises.unlink((0, import_node_path10.join)(stateDir, SEED_FILE_NAME)).catch((err2) => {
|
|
14364
|
+
if (err2.code !== "ENOENT") throw err2;
|
|
14365
|
+
seedFile = false;
|
|
14366
|
+
});
|
|
14367
|
+
const seed = await keychainDelete(KEYCHAIN_ACCOUNT);
|
|
14368
|
+
const legacy = await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
14369
|
+
return { seedFile, keychain: seed || legacy };
|
|
14370
|
+
}
|
|
14371
|
+
async function probeKeychain() {
|
|
14251
14372
|
const keytar = await loadKeytar();
|
|
14252
|
-
if (keytar === null) return false;
|
|
14373
|
+
if (keytar === null) return { available: false, hasSeed: false };
|
|
14253
14374
|
try {
|
|
14254
|
-
|
|
14375
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
14376
|
+
return { available: true, hasSeed: value !== null && value.length > 0 };
|
|
14255
14377
|
} catch {
|
|
14256
|
-
return false;
|
|
14378
|
+
return { available: false, hasSeed: false };
|
|
14257
14379
|
}
|
|
14258
14380
|
}
|
|
14259
|
-
async function promptHiddenPassphrase(prompt) {
|
|
14260
|
-
const stdin = process.stdin;
|
|
14261
|
-
if (!stdin.isTTY) {
|
|
14262
|
-
throw new Error(
|
|
14263
|
-
`Cannot prompt for passphrase: stdin is not a TTY. Set ${ENV_VAR} for non-interactive use.`
|
|
14264
|
-
);
|
|
14265
|
-
}
|
|
14266
|
-
process.stdout.write(prompt);
|
|
14267
|
-
const wasRaw = stdin.isRaw;
|
|
14268
|
-
stdin.setRawMode(true);
|
|
14269
|
-
stdin.resume();
|
|
14270
|
-
stdin.setEncoding("utf8");
|
|
14271
|
-
return new Promise((resolve5, reject) => {
|
|
14272
|
-
let buf = "";
|
|
14273
|
-
const cleanup = () => {
|
|
14274
|
-
stdin.removeListener("data", onData);
|
|
14275
|
-
stdin.setRawMode(wasRaw);
|
|
14276
|
-
stdin.pause();
|
|
14277
|
-
process.stdout.write("\n");
|
|
14278
|
-
};
|
|
14279
|
-
const onData = (chunk) => {
|
|
14280
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
14281
|
-
for (let i = 0; i < text.length; i++) {
|
|
14282
|
-
const ch = text[i];
|
|
14283
|
-
if (ch === "") {
|
|
14284
|
-
cleanup();
|
|
14285
|
-
reject(new Error("cancelled"));
|
|
14286
|
-
return;
|
|
14287
|
-
}
|
|
14288
|
-
if (ch === "" && buf.length === 0) {
|
|
14289
|
-
cleanup();
|
|
14290
|
-
reject(new Error("cancelled"));
|
|
14291
|
-
return;
|
|
14292
|
-
}
|
|
14293
|
-
if (ch === "\r" || ch === "\n") {
|
|
14294
|
-
cleanup();
|
|
14295
|
-
resolve5(buf);
|
|
14296
|
-
return;
|
|
14297
|
-
}
|
|
14298
|
-
if (ch === "\x7F" || ch === "\b") {
|
|
14299
|
-
buf = buf.slice(0, -1);
|
|
14300
|
-
continue;
|
|
14301
|
-
}
|
|
14302
|
-
buf += ch;
|
|
14303
|
-
}
|
|
14304
|
-
};
|
|
14305
|
-
stdin.on("data", onData);
|
|
14306
|
-
});
|
|
14307
|
-
}
|
|
14308
14381
|
// Annotate the CommonJS export names for ESM import in node:
|
|
14309
14382
|
0 && (module.exports = {
|
|
14310
14383
|
APP_NAME,
|
|
@@ -14312,16 +14385,20 @@ async function promptHiddenPassphrase(prompt) {
|
|
|
14312
14385
|
BackendError,
|
|
14313
14386
|
DEFAULT_BACKEND_URL,
|
|
14314
14387
|
DEFAULT_PUBLIC_HOST,
|
|
14388
|
+
EPHEMERAL_ENV_VAR,
|
|
14315
14389
|
HandoffBothChannelsFailedError,
|
|
14390
|
+
IdentityConflictError,
|
|
14316
14391
|
KEYCHAIN_ACCOUNT,
|
|
14317
14392
|
KEYCHAIN_SERVICE,
|
|
14318
|
-
KeytarUnavailableError,
|
|
14319
14393
|
LOCK_FILE_NAME,
|
|
14320
14394
|
PASSPHRASE_ENV_VAR,
|
|
14321
14395
|
PUBLIC_HOST_ENV_VAR,
|
|
14322
|
-
|
|
14396
|
+
SEED_ENV_VAR,
|
|
14397
|
+
SEED_FILE_NAME,
|
|
14323
14398
|
STATE_FILE_NAME,
|
|
14324
14399
|
SealApi,
|
|
14400
|
+
SeedInvalidError,
|
|
14401
|
+
StateBusyError,
|
|
14325
14402
|
StateCorrupt,
|
|
14326
14403
|
StateLockedError,
|
|
14327
14404
|
StatePassphraseInvalid,
|
|
@@ -14329,20 +14406,22 @@ async function promptHiddenPassphrase(prompt) {
|
|
|
14329
14406
|
acquireStateLock,
|
|
14330
14407
|
createEmptyState,
|
|
14331
14408
|
createServer,
|
|
14332
|
-
|
|
14409
|
+
deleteRootSecret,
|
|
14333
14410
|
generateHandle,
|
|
14334
14411
|
generateIdentity,
|
|
14412
|
+
holderMayRun,
|
|
14413
|
+
identityFromSeed,
|
|
14335
14414
|
identityToFields,
|
|
14336
14415
|
isValidHandle,
|
|
14337
14416
|
loadIdentity,
|
|
14338
14417
|
loadState,
|
|
14418
|
+
openAgentState,
|
|
14339
14419
|
performHandoff,
|
|
14340
14420
|
probeKeychain,
|
|
14341
14421
|
probeStateLock,
|
|
14342
|
-
|
|
14422
|
+
readSeed,
|
|
14343
14423
|
recoverPendingOperations,
|
|
14344
14424
|
resolveBackendUrl,
|
|
14345
|
-
resolvePassphraseFromStores,
|
|
14346
14425
|
resolvePublicHost,
|
|
14347
14426
|
resolveStateDir,
|
|
14348
14427
|
saveState,
|
|
@@ -14350,7 +14429,9 @@ async function promptHiddenPassphrase(prompt) {
|
|
|
14350
14429
|
sealOpen,
|
|
14351
14430
|
sealRevoke,
|
|
14352
14431
|
sealShare,
|
|
14353
|
-
|
|
14432
|
+
statePassphrase,
|
|
14433
|
+
updateState,
|
|
14434
|
+
withStateLock
|
|
14354
14435
|
});
|
|
14355
14436
|
/*! Bundled license information:
|
|
14356
14437
|
|