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.js
CHANGED
|
@@ -6,7 +6,6 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/server.ts
|
|
9
|
-
import { dirname as dirname3 } from "path";
|
|
10
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
11
|
|
|
@@ -4066,13 +4065,13 @@ var seal_mcp_v1_default = {
|
|
|
4066
4065
|
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`."
|
|
4067
4066
|
},
|
|
4068
4067
|
server_instructions: {
|
|
4069
|
-
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).",
|
|
4068
|
+
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.",
|
|
4070
4069
|
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."
|
|
4071
4070
|
},
|
|
4072
4071
|
tools: {
|
|
4073
4072
|
seal_share: {
|
|
4074
4073
|
title: "Share a file via SEAL",
|
|
4075
|
-
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for
|
|
4074
|
+
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.",
|
|
4076
4075
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
4077
4076
|
input: {
|
|
4078
4077
|
properties: {
|
|
@@ -4116,13 +4115,15 @@ var seal_mcp_v1_default = {
|
|
|
4116
4115
|
required: ["handle", "receipt", "handoff_channel"],
|
|
4117
4116
|
properties: {
|
|
4118
4117
|
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." },
|
|
4119
|
-
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 `
|
|
4118
|
+
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." },
|
|
4120
4119
|
handoff_channel: { type: "string", enum: ["clipboard", "file"] },
|
|
4121
4120
|
handoff_file: { type: "string", description: "ONLY when handoff_channel='file'. Absolute path to a 0o600 file containing the share link and a newline." },
|
|
4122
4121
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
4123
4122
|
},
|
|
4124
4123
|
MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
|
|
4125
|
-
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
|
|
4124
|
+
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode.",
|
|
4125
|
+
receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
|
|
4126
|
+
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."
|
|
4126
4127
|
},
|
|
4127
4128
|
output_forward: {
|
|
4128
4129
|
type: "object",
|
|
@@ -4450,7 +4451,7 @@ var seal_mcp_v1_default = {
|
|
|
4450
4451
|
type: "object",
|
|
4451
4452
|
required: ["x25519_secret_b64", "x25519_public_b64"],
|
|
4452
4453
|
properties: {
|
|
4453
|
-
x25519_secret_b64: { type: "string", description: "32 raw bytes,
|
|
4454
|
+
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." },
|
|
4454
4455
|
x25519_public_b64: { type: "string", description: "32 raw bytes, derived from secret. Safe to publish; advertised so other parties can target this MCP." }
|
|
4455
4456
|
}
|
|
4456
4457
|
},
|
|
@@ -4471,7 +4472,7 @@ var seal_mcp_v1_default = {
|
|
|
4471
4472
|
},
|
|
4472
4473
|
pending: {
|
|
4473
4474
|
type: "object",
|
|
4474
|
-
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.",
|
|
4475
|
+
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.",
|
|
4475
4476
|
additionalProperties: {
|
|
4476
4477
|
type: "object",
|
|
4477
4478
|
required: ["tool", "mode", "started_at", "owner_token_b64", "expires_at", "step"],
|
|
@@ -4484,6 +4485,12 @@ var seal_mcp_v1_default = {
|
|
|
4484
4485
|
step: { type: "string", enum: ["preparing", "awaiting_payment", "uploaded", "delivering", "compensating"] },
|
|
4485
4486
|
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." },
|
|
4486
4487
|
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)." },
|
|
4488
|
+
owner: {
|
|
4489
|
+
type: "object",
|
|
4490
|
+
required: ["pid", "hostname"],
|
|
4491
|
+
properties: { pid: { type: "integer" }, hostname: { type: "string" } },
|
|
4492
|
+
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."
|
|
4493
|
+
},
|
|
4487
4494
|
compensation: {
|
|
4488
4495
|
type: "object",
|
|
4489
4496
|
required: ["action", "outcome", "error", "at"],
|
|
@@ -4515,27 +4522,20 @@ var seal_mcp_v1_default = {
|
|
|
4515
4522
|
}
|
|
4516
4523
|
}
|
|
4517
4524
|
},
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
default: true
|
|
4525
|
-
},
|
|
4526
|
-
{
|
|
4527
|
-
rank: 2,
|
|
4528
|
-
method: "env_var",
|
|
4529
|
-
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."
|
|
4530
|
-
},
|
|
4531
|
-
{
|
|
4532
|
-
rank: 3,
|
|
4533
|
-
method: "REFUSED",
|
|
4534
|
-
implementation: "Cleartext passphrase file on disk is explicitly refused even if user passes `--passphrase-file \u2026`. Error message points at options 1 and 2."
|
|
4535
|
-
}
|
|
4525
|
+
root_secret: {
|
|
4526
|
+
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.",
|
|
4527
|
+
ordered_sources: [
|
|
4528
|
+
{ rank: 1, source: "env", implementation: "$SEAL_SEED. How a container or a cloud session gets the same address as at home." },
|
|
4529
|
+
{ 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." },
|
|
4530
|
+
{ 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." }
|
|
4536
4531
|
],
|
|
4537
|
-
|
|
4538
|
-
|
|
4532
|
+
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.",
|
|
4533
|
+
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.",
|
|
4534
|
+
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.",
|
|
4535
|
+
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.",
|
|
4536
|
+
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.",
|
|
4537
|
+
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`.",
|
|
4538
|
+
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."
|
|
4539
4539
|
},
|
|
4540
4540
|
handoff_delivery: {
|
|
4541
4541
|
primary_channel: "system_clipboard",
|
|
@@ -4611,11 +4611,12 @@ var seal_mcp_v1_default = {
|
|
|
4611
4611
|
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.",
|
|
4612
4612
|
pro_failed: "seal_pro_*: SEAL Pro answered with another error; the message has the status.",
|
|
4613
4613
|
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.",
|
|
4614
|
+
state_busy: "Another sealnet-mcp process held the state lock for more than 5 s. Nothing was changed by this call; safe to retry.",
|
|
4614
4615
|
runtime_unsupported: "Node runtime is missing a global the bundle assumes (e.g. `globalThis.File` or `globalThis.Blob`). Upgrade to Node \u226520.",
|
|
4615
4616
|
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.",
|
|
4616
|
-
|
|
4617
|
+
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`.",
|
|
4617
4618
|
"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.",
|
|
4618
|
-
state_dir_unsafe: "
|
|
4619
|
+
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.",
|
|
4619
4620
|
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."
|
|
4620
4621
|
},
|
|
4621
4622
|
current_format_version: 1,
|
|
@@ -11439,7 +11440,7 @@ function safeFilename(name) {
|
|
|
11439
11440
|
return cleaned.length > 0 ? cleaned : "seal-file";
|
|
11440
11441
|
}
|
|
11441
11442
|
async function receiveToDisk(opened, dir, name) {
|
|
11442
|
-
const
|
|
11443
|
+
const fs3 = await import(
|
|
11443
11444
|
/* webpackIgnore: true */
|
|
11444
11445
|
/* @vite-ignore */
|
|
11445
11446
|
"fs/promises"
|
|
@@ -11449,10 +11450,10 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11449
11450
|
/* @vite-ignore */
|
|
11450
11451
|
"path"
|
|
11451
11452
|
);
|
|
11452
|
-
await
|
|
11453
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
11453
11454
|
const suffix = Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11454
11455
|
const tmp = path.join(dir, `.seal-partial-${suffix}`);
|
|
11455
|
-
const fh = await
|
|
11456
|
+
const fh = await fs3.open(tmp, "wx", 384);
|
|
11456
11457
|
let placed = false;
|
|
11457
11458
|
try {
|
|
11458
11459
|
await opened.stream.pipeTo(
|
|
@@ -11470,12 +11471,12 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11470
11471
|
for (let i = 0; i < 1e3; i++) {
|
|
11471
11472
|
const candidate = path.join(dir, i === 0 ? name : `${stem} (${i})${ext}`);
|
|
11472
11473
|
try {
|
|
11473
|
-
await
|
|
11474
|
+
await fs3.link(tmp, candidate);
|
|
11474
11475
|
} catch (err2) {
|
|
11475
11476
|
if (err2.code === "EEXIST") continue;
|
|
11476
11477
|
throw err2;
|
|
11477
11478
|
}
|
|
11478
|
-
await
|
|
11479
|
+
await fs3.unlink(tmp);
|
|
11479
11480
|
placed = true;
|
|
11480
11481
|
return { path: candidate, sizeBytes, sha256: sha2562 };
|
|
11481
11482
|
}
|
|
@@ -11483,7 +11484,7 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11483
11484
|
} finally {
|
|
11484
11485
|
if (!placed) {
|
|
11485
11486
|
await fh.close().catch(() => void 0);
|
|
11486
|
-
await
|
|
11487
|
+
await fs3.unlink(tmp).catch(() => void 0);
|
|
11487
11488
|
}
|
|
11488
11489
|
}
|
|
11489
11490
|
}
|
|
@@ -11921,6 +11922,9 @@ async function generateIdentity() {
|
|
|
11921
11922
|
const { publicKey, secretKey } = await generateRecipientKeyPair();
|
|
11922
11923
|
return { secret: secretKey, public: publicKey };
|
|
11923
11924
|
}
|
|
11925
|
+
async function identityFromSeed(seed) {
|
|
11926
|
+
return { secret: seed, public: await recipientPublicKey(seed) };
|
|
11927
|
+
}
|
|
11924
11928
|
function identityToFields(id) {
|
|
11925
11929
|
return {
|
|
11926
11930
|
x25519_secret_b64: toB64(id.secret),
|
|
@@ -11954,20 +11958,27 @@ import { randomBytes } from "crypto";
|
|
|
11954
11958
|
import { hostname } from "os";
|
|
11955
11959
|
import { join as join2 } from "path";
|
|
11956
11960
|
var LOCK_FILE_NAME = "state.lock";
|
|
11961
|
+
var LOCK_WAIT_MS = 5e3;
|
|
11962
|
+
var LOCK_RETRY_MS = 25;
|
|
11957
11963
|
var StateLockedError = class extends Error {
|
|
11958
11964
|
code = "state_locked";
|
|
11959
11965
|
holder;
|
|
11960
11966
|
lockPath;
|
|
11961
11967
|
constructor(lockPath, holder) {
|
|
11962
11968
|
const who = holder === null ? "holder info unreadable" : `pid ${holder.pid} on ${holder.hostname} since ${holder.acquired_at}`;
|
|
11963
|
-
super(
|
|
11964
|
-
`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}`
|
|
11965
|
-
);
|
|
11969
|
+
super(`state directory is locked by another sealnet-mcp process (${who}). Lock file: ${lockPath}`);
|
|
11966
11970
|
this.name = "StateLockedError";
|
|
11967
11971
|
this.holder = holder;
|
|
11968
11972
|
this.lockPath = lockPath;
|
|
11969
11973
|
}
|
|
11970
11974
|
};
|
|
11975
|
+
var StateBusyError = class extends Error {
|
|
11976
|
+
code = "state_busy";
|
|
11977
|
+
constructor(locked) {
|
|
11978
|
+
super(`${locked.message}; waited ${LOCK_WAIT_MS / 1e3} s, nothing was changed, safe to retry`);
|
|
11979
|
+
this.name = "StateBusyError";
|
|
11980
|
+
}
|
|
11981
|
+
};
|
|
11971
11982
|
var StateDirUnsafeError = class extends Error {
|
|
11972
11983
|
code = "state_dir_unsafe";
|
|
11973
11984
|
constructor(stateDir, reason) {
|
|
@@ -11986,6 +11997,9 @@ function defaultIsPidAlive(pid) {
|
|
|
11986
11997
|
return code === "EPERM";
|
|
11987
11998
|
}
|
|
11988
11999
|
}
|
|
12000
|
+
function holderMayRun(holder, deps = {}) {
|
|
12001
|
+
return holder.hostname !== (deps.host ?? hostname()) || (deps.isPidAlive ?? defaultIsPidAlive)(holder.pid);
|
|
12002
|
+
}
|
|
11989
12003
|
function parseHolder(raw) {
|
|
11990
12004
|
try {
|
|
11991
12005
|
const parsed = JSON.parse(raw);
|
|
@@ -12028,7 +12042,7 @@ async function tryCreate(lockPath, info) {
|
|
|
12028
12042
|
await unlink(tmp).catch(() => void 0);
|
|
12029
12043
|
}
|
|
12030
12044
|
}
|
|
12031
|
-
async function tryBreakStale(lockPath, probed,
|
|
12045
|
+
async function tryBreakStale(lockPath, probed, deps) {
|
|
12032
12046
|
const claimPath = `${lockPath}.break-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
12033
12047
|
try {
|
|
12034
12048
|
await rename2(lockPath, claimPath);
|
|
@@ -12036,8 +12050,7 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
12036
12050
|
return true;
|
|
12037
12051
|
}
|
|
12038
12052
|
const claimed = parseHolder(await readFile2(claimPath, "utf8").catch(() => ""));
|
|
12039
|
-
|
|
12040
|
-
if (sameHolder(claimed, probed) || claimedStale) {
|
|
12053
|
+
if (sameHolder(claimed, probed) || claimed === null || !holderMayRun(claimed, deps)) {
|
|
12041
12054
|
await unlink(claimPath).catch(() => void 0);
|
|
12042
12055
|
return true;
|
|
12043
12056
|
}
|
|
@@ -12050,11 +12063,9 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
12050
12063
|
}
|
|
12051
12064
|
async function acquireStateLock(stateDir, deps = {}) {
|
|
12052
12065
|
const lockPath = join2(stateDir, LOCK_FILE_NAME);
|
|
12053
|
-
const isAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
12054
|
-
const selfHost = deps.host ?? hostname();
|
|
12055
12066
|
const self2 = {
|
|
12056
12067
|
pid: deps.pid ?? process.pid,
|
|
12057
|
-
hostname:
|
|
12068
|
+
hostname: deps.host ?? hostname(),
|
|
12058
12069
|
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12059
12070
|
};
|
|
12060
12071
|
await mkdir2(stateDir, { recursive: true, mode: 448 });
|
|
@@ -12065,11 +12076,10 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
12065
12076
|
await chmod(stateDir, 448).catch(() => void 0);
|
|
12066
12077
|
if (!await tryCreate(lockPath, self2)) {
|
|
12067
12078
|
const holder = await readHolder(lockPath);
|
|
12068
|
-
|
|
12069
|
-
if (holder !== null && !stale) {
|
|
12079
|
+
if (holder !== null && holderMayRun(holder, deps)) {
|
|
12070
12080
|
throw new StateLockedError(lockPath, holder);
|
|
12071
12081
|
}
|
|
12072
|
-
if (!await tryBreakStale(lockPath, holder,
|
|
12082
|
+
if (!await tryBreakStale(lockPath, holder, deps)) {
|
|
12073
12083
|
throw new StateLockedError(lockPath, await readHolder(lockPath));
|
|
12074
12084
|
}
|
|
12075
12085
|
if (!await tryCreate(lockPath, self2)) {
|
|
@@ -12101,6 +12111,30 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
12101
12111
|
}
|
|
12102
12112
|
};
|
|
12103
12113
|
}
|
|
12114
|
+
var queues = /* @__PURE__ */ new Map();
|
|
12115
|
+
function withStateLock(stateDir, fn, deps = {}) {
|
|
12116
|
+
const run = (queues.get(stateDir) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
12117
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
12118
|
+
let lock;
|
|
12119
|
+
for (; ; ) {
|
|
12120
|
+
try {
|
|
12121
|
+
lock = await acquireStateLock(stateDir, deps);
|
|
12122
|
+
break;
|
|
12123
|
+
} catch (err2) {
|
|
12124
|
+
if (!(err2 instanceof StateLockedError)) throw err2;
|
|
12125
|
+
if (Date.now() >= deadline) throw new StateBusyError(err2);
|
|
12126
|
+
await new Promise((resolve5) => setTimeout(resolve5, LOCK_RETRY_MS));
|
|
12127
|
+
}
|
|
12128
|
+
}
|
|
12129
|
+
try {
|
|
12130
|
+
return await fn();
|
|
12131
|
+
} finally {
|
|
12132
|
+
await lock.release();
|
|
12133
|
+
}
|
|
12134
|
+
});
|
|
12135
|
+
queues.set(stateDir, run);
|
|
12136
|
+
return run;
|
|
12137
|
+
}
|
|
12104
12138
|
async function probeStateLock(stateDir, deps = {}) {
|
|
12105
12139
|
const lockPath = join2(stateDir, LOCK_FILE_NAME);
|
|
12106
12140
|
const holder = await readHolder(lockPath);
|
|
@@ -12112,10 +12146,7 @@ async function probeStateLock(stateDir, deps = {}) {
|
|
|
12112
12146
|
return { held: false, holder: null, stale: false };
|
|
12113
12147
|
}
|
|
12114
12148
|
}
|
|
12115
|
-
|
|
12116
|
-
const sameHost = holder.hostname === (deps.host ?? hostname());
|
|
12117
|
-
const stale = sameHost && !isAlive(holder.pid);
|
|
12118
|
-
return { held: true, holder, stale };
|
|
12149
|
+
return { held: true, holder, stale: !holderMayRun(holder, deps) };
|
|
12119
12150
|
}
|
|
12120
12151
|
|
|
12121
12152
|
// src/recovery.ts
|
|
@@ -12132,6 +12163,10 @@ async function recoverPendingOperations(view, client, opts = {}) {
|
|
|
12132
12163
|
log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
|
|
12133
12164
|
continue;
|
|
12134
12165
|
}
|
|
12166
|
+
if (op.owner !== void 0 && op.owner.pid !== process.pid && holderMayRun(op.owner)) {
|
|
12167
|
+
log4(`[seal-mcp] recovery: left ${handle} alone \u2014 pid ${op.owner.pid} on ${op.owner.hostname} may still run it`);
|
|
12168
|
+
continue;
|
|
12169
|
+
}
|
|
12135
12170
|
if (op.seal_id === void 0) {
|
|
12136
12171
|
delete view.pending[handle];
|
|
12137
12172
|
report.dropped.push(handle);
|
|
@@ -12221,7 +12256,7 @@ var AES_KEY_BYTES = 32;
|
|
|
12221
12256
|
var AES_NONCE_BYTES = 12;
|
|
12222
12257
|
var StatePassphraseInvalid = class extends Error {
|
|
12223
12258
|
constructor() {
|
|
12224
|
-
super("
|
|
12259
|
+
super("AES-256-GCM tag check failed: another passphrase, or a tampered file");
|
|
12225
12260
|
this.name = "StatePassphraseInvalid";
|
|
12226
12261
|
}
|
|
12227
12262
|
};
|
|
@@ -12421,6 +12456,13 @@ async function saveState(state, passphrase, statePath) {
|
|
|
12421
12456
|
await fs.chmod(dir, 448).catch(() => {
|
|
12422
12457
|
});
|
|
12423
12458
|
}
|
|
12459
|
+
function updateState(passphrase, statePath, mutate) {
|
|
12460
|
+
return withStateLock(dirname2(statePath), async () => {
|
|
12461
|
+
const view = await loadState(passphrase, statePath);
|
|
12462
|
+
if (await mutate(view) !== false) await saveState(view, passphrase, statePath);
|
|
12463
|
+
return view;
|
|
12464
|
+
});
|
|
12465
|
+
}
|
|
12424
12466
|
|
|
12425
12467
|
// src/tools/context.ts
|
|
12426
12468
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
@@ -12438,19 +12480,18 @@ var HANDLE_REGEX = /^h_[A-Za-z0-9_-]{12,}$/;
|
|
|
12438
12480
|
function isValidHandle(handle) {
|
|
12439
12481
|
return typeof handle === "string" && HANDLE_REGEX.test(handle);
|
|
12440
12482
|
}
|
|
12483
|
+
function auditEntry(ctx, tool, handle, mode) {
|
|
12484
|
+
return { ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(), tool, handle, mode };
|
|
12485
|
+
}
|
|
12441
12486
|
function appendAudit(ctx, tool, handle, mode) {
|
|
12442
|
-
ctx
|
|
12443
|
-
|
|
12444
|
-
tool,
|
|
12445
|
-
handle,
|
|
12446
|
-
mode
|
|
12447
|
-
});
|
|
12487
|
+
const entry = auditEntry(ctx, tool, handle, mode);
|
|
12488
|
+
return ctx.update((state) => void state.audit.push(entry));
|
|
12448
12489
|
}
|
|
12449
|
-
function insertHandle(
|
|
12450
|
-
if (
|
|
12490
|
+
function insertHandle(state, handle, entry) {
|
|
12491
|
+
if (state.handles[handle]) {
|
|
12451
12492
|
throw new Error(`insertHandle: collision on ${handle}`);
|
|
12452
12493
|
}
|
|
12453
|
-
|
|
12494
|
+
state.handles[handle] = entry;
|
|
12454
12495
|
}
|
|
12455
12496
|
function lookupHandle(ctx, handle) {
|
|
12456
12497
|
const entry = ctx.state.handles[handle];
|
|
@@ -12473,6 +12514,7 @@ var ToolError = class extends Error {
|
|
|
12473
12514
|
// src/tools/list.ts
|
|
12474
12515
|
var SINGLE_FILE_COUNT = 1;
|
|
12475
12516
|
async function sealList(ctx, input = {}) {
|
|
12517
|
+
await appendAudit(ctx, "seal_list", null, null);
|
|
12476
12518
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
12477
12519
|
const entries = [];
|
|
12478
12520
|
for (const [handle, entry] of Object.entries(ctx.state.handles)) {
|
|
@@ -12489,8 +12531,6 @@ async function sealList(ctx, input = {}) {
|
|
|
12489
12531
|
});
|
|
12490
12532
|
}
|
|
12491
12533
|
entries.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
12492
|
-
appendAudit(ctx, "seal_list", null, null);
|
|
12493
|
-
await ctx.persist();
|
|
12494
12534
|
return { entries };
|
|
12495
12535
|
}
|
|
12496
12536
|
|
|
@@ -12575,13 +12615,11 @@ async function sealOpen(ctx, input) {
|
|
|
12575
12615
|
if (mode === "inline") {
|
|
12576
12616
|
const refusal = classifyInlineRefusal(first.mime);
|
|
12577
12617
|
if (refusal !== null) {
|
|
12578
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12579
|
-
await ctx.persist();
|
|
12618
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12580
12619
|
return { handle, refusal, mime: first.mime, note: refusalNote(refusal, first.mime) };
|
|
12581
12620
|
}
|
|
12582
12621
|
if (first.size > INLINE_CIPHERTEXT_MAX_BYTES) {
|
|
12583
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12584
|
-
await ctx.persist();
|
|
12622
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12585
12623
|
return {
|
|
12586
12624
|
handle,
|
|
12587
12625
|
refusal: "inline_blocked.too_large",
|
|
@@ -12617,12 +12655,10 @@ async function sealOpen(ctx, input) {
|
|
|
12617
12655
|
err2
|
|
12618
12656
|
);
|
|
12619
12657
|
}
|
|
12620
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12621
|
-
await ctx.persist();
|
|
12658
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12622
12659
|
return { handle, mime: bareMime(first.mime), sizeBytes: plaintext.byteLength, encoding: "utf-8", content };
|
|
12623
12660
|
}
|
|
12624
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12625
|
-
await ctx.persist();
|
|
12661
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12626
12662
|
return {
|
|
12627
12663
|
handle,
|
|
12628
12664
|
filename: first.name,
|
|
@@ -12668,8 +12704,7 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12668
12704
|
throw streamError(err2, file.id);
|
|
12669
12705
|
}
|
|
12670
12706
|
}
|
|
12671
|
-
appendAudit(ctx, "seal_open", handle, "file");
|
|
12672
|
-
await ctx.persist();
|
|
12707
|
+
await appendAudit(ctx, "seal_open", handle, "file");
|
|
12673
12708
|
return { handle, files: out };
|
|
12674
12709
|
}
|
|
12675
12710
|
|
|
@@ -12772,8 +12807,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
12772
12807
|
if (input.whereUrl) cliArgs.push("--where", input.whereUrl);
|
|
12773
12808
|
const local = await (deps.runCli ?? runSealCli)(cliArgs);
|
|
12774
12809
|
if (local && "files" in local) {
|
|
12775
|
-
appendAudit(ctx, "seal_request", handle, "local");
|
|
12776
|
-
await ctx.persist();
|
|
12810
|
+
await appendAudit(ctx, "seal_request", handle, "local");
|
|
12777
12811
|
return {
|
|
12778
12812
|
handle,
|
|
12779
12813
|
source: "local",
|
|
@@ -12813,8 +12847,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
12813
12847
|
}
|
|
12814
12848
|
return receiveThroughLink(ctx, handle, intake.id, deps, input.onProgress);
|
|
12815
12849
|
}
|
|
12816
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
12817
|
-
await ctx.persist();
|
|
12850
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
12818
12851
|
return { handle, url, emoji, expiresAt: intake.expires_at };
|
|
12819
12852
|
}
|
|
12820
12853
|
async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
@@ -12835,8 +12868,7 @@ async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
|
12835
12868
|
const opened = await sealOpen(ctx, { url: `${deps.publicHost}${sharePath}`, mode: "file", dir, onProgress });
|
|
12836
12869
|
if (!("files" in opened)) throw new ToolError("internal_inconsistency", "seal_open mode=file returned no files");
|
|
12837
12870
|
ctx.requests?.delete(handle);
|
|
12838
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
12839
|
-
await ctx.persist();
|
|
12871
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
12840
12872
|
return {
|
|
12841
12873
|
handle,
|
|
12842
12874
|
source: "link",
|
|
@@ -12849,6 +12881,7 @@ async function sealRevoke(ctx, input) {
|
|
|
12849
12881
|
if (!isValidHandle(input.handle)) {
|
|
12850
12882
|
throw new ToolError("handle_invalid", `handle does not match contract pattern: ${input.handle}`);
|
|
12851
12883
|
}
|
|
12884
|
+
await ctx.update(() => false);
|
|
12852
12885
|
const entry = lookupHandle(ctx, input.handle);
|
|
12853
12886
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
12854
12887
|
if (entry.revoked_at) {
|
|
@@ -12883,15 +12916,17 @@ async function sealRevoke(ctx, input) {
|
|
|
12883
12916
|
err2
|
|
12884
12917
|
);
|
|
12885
12918
|
}
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12919
|
+
await ctx.update((state) => {
|
|
12920
|
+
state.handles[input.handle] = { ...entry, revoked_at: now };
|
|
12921
|
+
state.audit.push(auditEntry(ctx, "seal_revoke", input.handle, entry.mode));
|
|
12922
|
+
});
|
|
12889
12923
|
return { revoked: true, revokedAt: now };
|
|
12890
12924
|
}
|
|
12891
12925
|
|
|
12892
12926
|
// src/tools/share.ts
|
|
12893
12927
|
import { openAsBlob as openAsBlob2 } from "fs";
|
|
12894
12928
|
import { stat as stat3 } from "fs/promises";
|
|
12929
|
+
import { hostname as hostname2 } from "os";
|
|
12895
12930
|
import { basename as basename3 } from "path";
|
|
12896
12931
|
|
|
12897
12932
|
// ../../contracts/limits.json
|
|
@@ -13118,6 +13153,7 @@ function guessMimeFromName(name) {
|
|
|
13118
13153
|
}
|
|
13119
13154
|
var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
|
|
13120
13155
|
var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
|
|
13156
|
+
var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
|
|
13121
13157
|
var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
|
|
13122
13158
|
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.";
|
|
13123
13159
|
function defaultExpire(mode, size) {
|
|
@@ -13145,31 +13181,46 @@ var FORWARD_MAX_READS_FORCED = 1;
|
|
|
13145
13181
|
function nowIso2(ctx) {
|
|
13146
13182
|
return (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13147
13183
|
}
|
|
13148
|
-
|
|
13184
|
+
function journal(ctx, handle, op) {
|
|
13185
|
+
return ctx.update((state) => {
|
|
13186
|
+
state.pending[handle] = op;
|
|
13187
|
+
});
|
|
13188
|
+
}
|
|
13189
|
+
function dropJournal(ctx, handle) {
|
|
13190
|
+
return ctx.update((state) => {
|
|
13191
|
+
delete state.pending[handle];
|
|
13192
|
+
});
|
|
13193
|
+
}
|
|
13194
|
+
function complete(ctx, handle, entry) {
|
|
13195
|
+
return ctx.update((state) => {
|
|
13196
|
+
insertHandle(state, handle, entry);
|
|
13197
|
+
state.audit.push(auditEntry(ctx, "seal_share", handle, entry.mode));
|
|
13198
|
+
delete state.pending[handle];
|
|
13199
|
+
});
|
|
13200
|
+
}
|
|
13201
|
+
async function bestEffort(write) {
|
|
13149
13202
|
try {
|
|
13150
|
-
await
|
|
13203
|
+
await write;
|
|
13151
13204
|
} catch (err2) {
|
|
13152
13205
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13153
|
-
process.stderr.write(`[seal-mcp] warn: journal
|
|
13206
|
+
process.stderr.write(`[seal-mcp] warn: journal write failed: ${msg}
|
|
13154
13207
|
`);
|
|
13155
13208
|
}
|
|
13156
13209
|
}
|
|
13157
|
-
async function compensateRevoke(ctx, handle) {
|
|
13158
|
-
const
|
|
13159
|
-
|
|
13160
|
-
if (!op || sealId === void 0) {
|
|
13210
|
+
async function compensateRevoke(ctx, handle, op) {
|
|
13211
|
+
const sealId = op.seal_id;
|
|
13212
|
+
if (sealId === void 0) {
|
|
13161
13213
|
return "no live seal to revoke";
|
|
13162
13214
|
}
|
|
13163
13215
|
try {
|
|
13164
13216
|
await ctx.client.revoke(sealId, op.owner_token_b64);
|
|
13165
|
-
|
|
13166
|
-
await persistBestEffort(ctx);
|
|
13217
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13167
13218
|
return "seal auto-revoked";
|
|
13168
13219
|
} catch (err2) {
|
|
13169
13220
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13170
13221
|
op.step = "compensating";
|
|
13171
13222
|
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso2(ctx) };
|
|
13172
|
-
await
|
|
13223
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13173
13224
|
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`;
|
|
13174
13225
|
}
|
|
13175
13226
|
}
|
|
@@ -13341,13 +13392,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13341
13392
|
// backend this seal is actually created on — recovery refuses to
|
|
13342
13393
|
// "revoke" against a different backend whose 404 would be misread
|
|
13343
13394
|
// as success.
|
|
13344
|
-
backend_url: ctx.client.baseUrl
|
|
13395
|
+
backend_url: ctx.client.baseUrl,
|
|
13396
|
+
// SPEC-ADDRESS F1 §4: another MCP host starting now must not recover
|
|
13397
|
+
// (revoke) this upload while it runs.
|
|
13398
|
+
owner: { pid: process.pid, hostname: hostname2() }
|
|
13345
13399
|
};
|
|
13346
|
-
ctx.state.pending[handle] = op;
|
|
13347
13400
|
try {
|
|
13348
|
-
await ctx
|
|
13401
|
+
await journal(ctx, handle, op);
|
|
13349
13402
|
} catch (err2) {
|
|
13350
|
-
delete ctx.state.pending[handle];
|
|
13351
13403
|
throw new ToolError(
|
|
13352
13404
|
"state_persist_failed",
|
|
13353
13405
|
`refusing to upload: could not journal the operation before its first remote effect: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13368,7 +13420,7 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13368
13420
|
// `uploaded` checkpoint after this call is the durability gate.
|
|
13369
13421
|
onSealCreated: async (sealId) => {
|
|
13370
13422
|
op.seal_id = sealId;
|
|
13371
|
-
await
|
|
13423
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13372
13424
|
}
|
|
13373
13425
|
});
|
|
13374
13426
|
} catch (err2) {
|
|
@@ -13377,15 +13429,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13377
13429
|
return awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, expiresAt, session });
|
|
13378
13430
|
}
|
|
13379
13431
|
if (op.seal_id !== void 0) {
|
|
13380
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13432
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13381
13433
|
throw new ToolError(
|
|
13382
13434
|
"upload_failed",
|
|
13383
13435
|
`seal upload failed after the seal was created: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13384
13436
|
err2
|
|
13385
13437
|
);
|
|
13386
13438
|
}
|
|
13387
|
-
|
|
13388
|
-
await persistBestEffort(ctx);
|
|
13439
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13389
13440
|
throw new ToolError(
|
|
13390
13441
|
"upload_failed",
|
|
13391
13442
|
`seal upload failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13398,9 +13449,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13398
13449
|
op.seal_id = uploaded.sealId;
|
|
13399
13450
|
op.step = "uploaded";
|
|
13400
13451
|
try {
|
|
13401
|
-
await ctx
|
|
13452
|
+
await journal(ctx, handle, op);
|
|
13402
13453
|
} catch (err2) {
|
|
13403
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13454
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13404
13455
|
throw new ToolError(
|
|
13405
13456
|
"state_persist_failed",
|
|
13406
13457
|
`could not journal the live seal after upload: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13413,7 +13464,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13413
13464
|
try {
|
|
13414
13465
|
shareBlobBytes = await encryptShareBlob(fileKeysB64, shareLinkKey, void 0);
|
|
13415
13466
|
} catch (err2) {
|
|
13416
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13467
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13417
13468
|
throw new ToolError(
|
|
13418
13469
|
"share_blob_failed",
|
|
13419
13470
|
`share blob encryption failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13425,7 +13476,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13425
13476
|
try {
|
|
13426
13477
|
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13427
13478
|
} catch (err2) {
|
|
13428
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13479
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13429
13480
|
throw new ToolError(
|
|
13430
13481
|
"share_link_failed",
|
|
13431
13482
|
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13443,7 +13494,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13443
13494
|
try {
|
|
13444
13495
|
fragment = await buildTargetedFragment(shareLinkKey, recipientPub, uploaded.sealId);
|
|
13445
13496
|
} catch (err2) {
|
|
13446
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13497
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13447
13498
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
13448
13499
|
if (message.includes("non-contributory")) {
|
|
13449
13500
|
throw new ToolError(
|
|
@@ -13464,7 +13515,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13464
13515
|
const host = resolvePublicHost(deps.publicHost);
|
|
13465
13516
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13466
13517
|
if (mode === "forward") {
|
|
13467
|
-
|
|
13518
|
+
await complete(ctx, handle, {
|
|
13468
13519
|
seal_id: uploaded.sealId,
|
|
13469
13520
|
owner_token_b64: ownerTokenB64,
|
|
13470
13521
|
created_at: nowIso2(ctx),
|
|
@@ -13472,9 +13523,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13472
13523
|
expires_at: expiresAt,
|
|
13473
13524
|
backend_url: ctx.client.baseUrl
|
|
13474
13525
|
});
|
|
13475
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13476
|
-
delete ctx.state.pending[handle];
|
|
13477
|
-
await ctx.persist();
|
|
13478
13526
|
return {
|
|
13479
13527
|
handle,
|
|
13480
13528
|
shareUrl,
|
|
@@ -13486,9 +13534,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13486
13534
|
op.step = "delivering";
|
|
13487
13535
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13488
13536
|
try {
|
|
13489
|
-
await ctx
|
|
13537
|
+
await journal(ctx, handle, op);
|
|
13490
13538
|
} catch (err2) {
|
|
13491
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13539
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13492
13540
|
throw new ToolError(
|
|
13493
13541
|
"state_persist_failed",
|
|
13494
13542
|
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13499,14 +13547,14 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13499
13547
|
try {
|
|
13500
13548
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
13501
13549
|
} catch (err2) {
|
|
13502
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13550
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13503
13551
|
throw new ToolError(
|
|
13504
13552
|
"handoff_both_channels_failed",
|
|
13505
13553
|
`both clipboard and file handoff channels failed \u2014 ${outcome}`,
|
|
13506
13554
|
err2
|
|
13507
13555
|
);
|
|
13508
13556
|
}
|
|
13509
|
-
|
|
13557
|
+
await complete(ctx, handle, {
|
|
13510
13558
|
seal_id: uploaded.sealId,
|
|
13511
13559
|
owner_token_b64: ownerTokenB64,
|
|
13512
13560
|
created_at: nowIso2(ctx),
|
|
@@ -13515,10 +13563,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13515
13563
|
handoff_channel: handoff.channel,
|
|
13516
13564
|
backend_url: ctx.client.baseUrl
|
|
13517
13565
|
});
|
|
13518
|
-
|
|
13519
|
-
delete ctx.state.pending[handle];
|
|
13520
|
-
await ctx.persist();
|
|
13521
|
-
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.`;
|
|
13566
|
+
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13522
13567
|
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.`;
|
|
13523
13568
|
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;
|
|
13524
13569
|
return {
|
|
@@ -13536,12 +13581,12 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13536
13581
|
const tier = prepared.tier;
|
|
13537
13582
|
const sealId = op.seal_id;
|
|
13538
13583
|
op.step = "awaiting_payment";
|
|
13539
|
-
await
|
|
13584
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13540
13585
|
let paymentUrl;
|
|
13541
13586
|
try {
|
|
13542
13587
|
paymentUrl = await ctx.client.createCheckout(sealId, ownerTokenB64, tier);
|
|
13543
13588
|
} catch (err2) {
|
|
13544
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13589
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13545
13590
|
throw new ToolError("payment_failed", `POST /billing/checkout failed: ${messageOf3(err2)} (${outcome})`, err2);
|
|
13546
13591
|
}
|
|
13547
13592
|
const deadline = paymentDeadline(op.expires_at);
|
|
@@ -13551,7 +13596,7 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13551
13596
|
clearTimeout(timer);
|
|
13552
13597
|
ctx.payments?.delete(handle);
|
|
13553
13598
|
await prepared.cleanup();
|
|
13554
|
-
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle)})`);
|
|
13599
|
+
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle, op)})`);
|
|
13555
13600
|
};
|
|
13556
13601
|
const finish = async (onProgress) => {
|
|
13557
13602
|
if (running) throw new ToolError("payment_invalid", `share ${handle} is already being finished`);
|
|
@@ -13720,7 +13765,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13720
13765
|
}
|
|
13721
13766
|
|
|
13722
13767
|
// src/server.ts
|
|
13723
|
-
var SERVER_VERSION = "0.
|
|
13768
|
+
var SERVER_VERSION = "0.3.0";
|
|
13724
13769
|
var toolMeta = (name) => {
|
|
13725
13770
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13726
13771
|
return { title, description, annotations };
|
|
@@ -13747,41 +13792,29 @@ function progressReporter(extra) {
|
|
|
13747
13792
|
}
|
|
13748
13793
|
async function createServer(options) {
|
|
13749
13794
|
logEngineToStderr();
|
|
13750
|
-
let view;
|
|
13751
|
-
let identity;
|
|
13752
|
-
let lock = null;
|
|
13753
13795
|
const client = new SealApi({ baseUrl: resolveBackendUrl(options.backendBaseUrl) });
|
|
13754
13796
|
const disk = options.state;
|
|
13797
|
+
let view;
|
|
13798
|
+
const update = async (mutate) => {
|
|
13799
|
+
if (disk === null) await mutate(view);
|
|
13800
|
+
else view = await updateState(disk.passphrase, disk.path, mutate);
|
|
13801
|
+
};
|
|
13802
|
+
let identity;
|
|
13755
13803
|
if (disk === null) {
|
|
13756
13804
|
identity = await generateIdentity();
|
|
13757
13805
|
view = createEmptyState(identityToFields(identity));
|
|
13758
13806
|
} else {
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
try {
|
|
13762
|
-
view = await loadState(disk.passphrase, disk.path);
|
|
13763
|
-
identity = await loadIdentity(view.identity);
|
|
13764
|
-
const { changed } = await recoverPendingOperations(view, client);
|
|
13765
|
-
if (changed) {
|
|
13766
|
-
await saveState(view, disk.passphrase, disk.path);
|
|
13767
|
-
}
|
|
13768
|
-
} catch (err2) {
|
|
13769
|
-
await held.release();
|
|
13770
|
-
throw err2;
|
|
13771
|
-
}
|
|
13807
|
+
view = await updateState(disk.passphrase, disk.path, async (state) => (await recoverPendingOperations(state, client)).changed);
|
|
13808
|
+
identity = await loadIdentity(view.identity);
|
|
13772
13809
|
}
|
|
13773
13810
|
const publicHost = resolvePublicHost(options.publicHost);
|
|
13774
|
-
let persistChain = Promise.resolve();
|
|
13775
|
-
const persist = () => {
|
|
13776
|
-
if (disk === null) return Promise.resolve();
|
|
13777
|
-
persistChain = persistChain.catch(() => void 0).then(() => saveState(view, disk.passphrase, disk.path));
|
|
13778
|
-
return persistChain;
|
|
13779
|
-
};
|
|
13780
13811
|
const ctx = {
|
|
13781
|
-
state
|
|
13812
|
+
get state() {
|
|
13813
|
+
return view;
|
|
13814
|
+
},
|
|
13782
13815
|
identity,
|
|
13783
13816
|
client,
|
|
13784
|
-
|
|
13817
|
+
update,
|
|
13785
13818
|
ephemeral: disk === null,
|
|
13786
13819
|
requests: /* @__PURE__ */ new Map(),
|
|
13787
13820
|
payments: /* @__PURE__ */ new Map()
|
|
@@ -13984,18 +14017,12 @@ async function createServer(options) {
|
|
|
13984
14017
|
);
|
|
13985
14018
|
}
|
|
13986
14019
|
const transport = new StdioServerTransport();
|
|
13987
|
-
|
|
13988
|
-
await mcp.connect(transport);
|
|
13989
|
-
} catch (err2) {
|
|
13990
|
-
await lock?.release();
|
|
13991
|
-
throw err2;
|
|
13992
|
-
}
|
|
14020
|
+
await mcp.connect(transport);
|
|
13993
14021
|
const close = async () => {
|
|
13994
14022
|
try {
|
|
13995
14023
|
await mcp.close();
|
|
13996
14024
|
} finally {
|
|
13997
14025
|
await transport.close().catch(() => void 0);
|
|
13998
|
-
await lock?.release();
|
|
13999
14026
|
}
|
|
14000
14027
|
};
|
|
14001
14028
|
return { mcp, transport, close };
|
|
@@ -14009,7 +14036,7 @@ async function wrapTool(fn, toWire) {
|
|
|
14009
14036
|
structuredContent: wire
|
|
14010
14037
|
};
|
|
14011
14038
|
} catch (err2) {
|
|
14012
|
-
if (err2 instanceof ToolError) {
|
|
14039
|
+
if (err2 instanceof ToolError || err2 instanceof StateBusyError) {
|
|
14013
14040
|
return {
|
|
14014
14041
|
isError: true,
|
|
14015
14042
|
content: [
|
|
@@ -14068,29 +14095,34 @@ function osDefaultDir(env, platform, home) {
|
|
|
14068
14095
|
}
|
|
14069
14096
|
}
|
|
14070
14097
|
|
|
14071
|
-
// src/
|
|
14098
|
+
// src/seed.ts
|
|
14072
14099
|
import { execFile as execFile2 } from "child_process";
|
|
14073
|
-
import {
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
var
|
|
14077
|
-
var
|
|
14078
|
-
var
|
|
14079
|
-
var
|
|
14080
|
-
var
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
};
|
|
14087
|
-
var PassphraseNotFoundError = class extends Error {
|
|
14088
|
-
code = "passphrase_not_found";
|
|
14089
|
-
constructor() {
|
|
14100
|
+
import { hkdfSync } from "crypto";
|
|
14101
|
+
import { promises as fs2 } from "fs";
|
|
14102
|
+
import { join as join9 } from "path";
|
|
14103
|
+
var SEED_ENV_VAR = "SEAL_SEED";
|
|
14104
|
+
var EPHEMERAL_ENV_VAR = "SEAL_MCP_EPHEMERAL";
|
|
14105
|
+
var PASSPHRASE_ENV_VAR = "SEAL_MCP_PASSPHRASE";
|
|
14106
|
+
var SEED_FILE_NAME = "seed";
|
|
14107
|
+
var KEYCHAIN_SERVICE = "seal-mcp";
|
|
14108
|
+
var KEYCHAIN_ACCOUNT = "seed";
|
|
14109
|
+
var LEGACY_KEYCHAIN_ACCOUNT = "state";
|
|
14110
|
+
var SeedInvalidError = class extends Error {
|
|
14111
|
+
code = "seed_invalid";
|
|
14112
|
+
constructor(where) {
|
|
14090
14113
|
super(
|
|
14091
|
-
|
|
14114
|
+
`${where} is not an agent key: expected 43 base64url characters, as \`sealnet-mcp seed --force\` or \`seal keygen --export --force\` print them`
|
|
14092
14115
|
);
|
|
14093
|
-
this.name = "
|
|
14116
|
+
this.name = "SeedInvalidError";
|
|
14117
|
+
}
|
|
14118
|
+
};
|
|
14119
|
+
var IdentityConflictError = class extends Error {
|
|
14120
|
+
code = "identity_conflict";
|
|
14121
|
+
constructor(statePath) {
|
|
14122
|
+
super(
|
|
14123
|
+
`${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.`
|
|
14124
|
+
);
|
|
14125
|
+
this.name = "IdentityConflictError";
|
|
14094
14126
|
}
|
|
14095
14127
|
};
|
|
14096
14128
|
function keyringEntry(keyring, platform, service, account) {
|
|
@@ -14125,113 +14157,146 @@ async function loadKeytar() {
|
|
|
14125
14157
|
try {
|
|
14126
14158
|
const mod = await import("@napi-rs/keyring");
|
|
14127
14159
|
const keyring = mod.default ?? mod;
|
|
14128
|
-
keyringEntry(keyring, process.platform,
|
|
14160
|
+
keyringEntry(keyring, process.platform, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
14129
14161
|
keytarCache = keytarOverKeyring(keyring);
|
|
14130
14162
|
} catch {
|
|
14131
14163
|
keytarCache = null;
|
|
14132
14164
|
}
|
|
14133
14165
|
return keytarCache;
|
|
14134
14166
|
}
|
|
14135
|
-
async function
|
|
14136
|
-
const
|
|
14137
|
-
if (
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14167
|
+
async function keychainGet(account) {
|
|
14168
|
+
const keytar = await loadKeytar();
|
|
14169
|
+
if (keytar === null) return null;
|
|
14170
|
+
try {
|
|
14171
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, account);
|
|
14172
|
+
return value !== null && value.length > 0 ? value : null;
|
|
14173
|
+
} catch {
|
|
14174
|
+
return null;
|
|
14175
|
+
}
|
|
14176
|
+
}
|
|
14177
|
+
async function keychainDelete(account) {
|
|
14178
|
+
const keytar = await loadKeytar();
|
|
14179
|
+
if (keytar === null) return false;
|
|
14180
|
+
try {
|
|
14181
|
+
return await keytar.deletePassword(KEYCHAIN_SERVICE, account);
|
|
14182
|
+
} catch {
|
|
14183
|
+
return false;
|
|
14184
|
+
}
|
|
14185
|
+
}
|
|
14186
|
+
function parseSeed(text, where) {
|
|
14187
|
+
const seed = Buffer.from(text, "base64url");
|
|
14188
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(text) || seed.toString("base64url") !== text) throw new SeedInvalidError(where);
|
|
14189
|
+
return new Uint8Array(seed);
|
|
14190
|
+
}
|
|
14191
|
+
async function readSeed(stateDir, env = process.env) {
|
|
14192
|
+
const fromEnv = env[SEED_ENV_VAR]?.trim();
|
|
14193
|
+
if (fromEnv) return { seed: parseSeed(fromEnv, SEED_ENV_VAR), source: "env" };
|
|
14194
|
+
const stored = await keychainGet(KEYCHAIN_ACCOUNT);
|
|
14195
|
+
if (stored !== null) return { seed: parseSeed(stored, `keychain entry ${KEYCHAIN_SERVICE}/${KEYCHAIN_ACCOUNT}`), source: "keychain" };
|
|
14196
|
+
const file = join9(stateDir, SEED_FILE_NAME);
|
|
14197
|
+
let text;
|
|
14198
|
+
try {
|
|
14199
|
+
text = await fs2.readFile(file, "utf8");
|
|
14200
|
+
} catch (err2) {
|
|
14201
|
+
if (err2.code === "ENOENT") return null;
|
|
14202
|
+
throw err2;
|
|
14141
14203
|
}
|
|
14142
|
-
return
|
|
14204
|
+
return { seed: parseSeed(text.trim(), file), source: "file" };
|
|
14143
14205
|
}
|
|
14144
|
-
async function
|
|
14206
|
+
async function storeSeed(stateDir, seed) {
|
|
14207
|
+
const text = Buffer.from(seed).toString("base64url");
|
|
14145
14208
|
const keytar = await loadKeytar();
|
|
14146
14209
|
if (keytar !== null) {
|
|
14147
14210
|
try {
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
return { passphrase: stored, source: "keychain" };
|
|
14151
|
-
}
|
|
14211
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, text);
|
|
14212
|
+
return "keychain";
|
|
14152
14213
|
} catch {
|
|
14153
14214
|
}
|
|
14154
14215
|
}
|
|
14155
|
-
const
|
|
14156
|
-
|
|
14157
|
-
|
|
14216
|
+
const fh = await fs2.open(join9(stateDir, SEED_FILE_NAME), "wx", 384);
|
|
14217
|
+
try {
|
|
14218
|
+
await fh.writeFile(`${text}
|
|
14219
|
+
`, "utf8");
|
|
14220
|
+
await fh.sync();
|
|
14221
|
+
} finally {
|
|
14222
|
+
await fh.close();
|
|
14158
14223
|
}
|
|
14159
|
-
return
|
|
14224
|
+
return "file";
|
|
14160
14225
|
}
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14226
|
+
function statePassphrase(seed) {
|
|
14227
|
+
return Buffer.from(hkdfSync("sha256", seed, "seal-mcp:state:v1", "passphrase", 32)).toString("base64url");
|
|
14228
|
+
}
|
|
14229
|
+
async function legacyPassphrase(env) {
|
|
14230
|
+
const stored = await keychainGet(LEGACY_KEYCHAIN_ACCOUNT);
|
|
14231
|
+
if (stored !== null) return { passphrase: stored, source: "keychain" };
|
|
14232
|
+
const fromEnv = env[PASSPHRASE_ENV_VAR];
|
|
14233
|
+
return fromEnv ? { passphrase: fromEnv, source: "env" } : null;
|
|
14234
|
+
}
|
|
14235
|
+
async function tryOpen(passphrase, statePath) {
|
|
14164
14236
|
try {
|
|
14165
|
-
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
|
|
14237
|
+
return await loadState(passphrase, statePath);
|
|
14238
|
+
} catch (err2) {
|
|
14239
|
+
if (err2 instanceof StatePassphraseInvalid) return null;
|
|
14240
|
+
throw err2;
|
|
14169
14241
|
}
|
|
14170
14242
|
}
|
|
14171
|
-
async function
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
}
|
|
14175
|
-
const keytar = await loadKeytarOrThrow();
|
|
14176
|
-
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, passphrase);
|
|
14243
|
+
async function requireIdentity(view, seed, statePath) {
|
|
14244
|
+
const { secret } = await loadIdentity(view.identity);
|
|
14245
|
+
if (!Buffer.from(secret).equals(Buffer.from(seed))) throw new IdentityConflictError(statePath);
|
|
14177
14246
|
}
|
|
14178
|
-
|
|
14247
|
+
function openAgentState(path, env = process.env) {
|
|
14248
|
+
return withStateLock(path.dir, async () => {
|
|
14249
|
+
let root = await readSeed(path.dir, env);
|
|
14250
|
+
if (!await fs2.stat(path.statePath).then(() => true, () => false)) {
|
|
14251
|
+
if (root === null) {
|
|
14252
|
+
const { secret } = await generateIdentity();
|
|
14253
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
14254
|
+
}
|
|
14255
|
+
const passphrase2 = statePassphrase(root.seed);
|
|
14256
|
+
const identity = await identityFromSeed(root.seed);
|
|
14257
|
+
await saveState(createEmptyState(identityToFields(identity)), passphrase2, path.statePath);
|
|
14258
|
+
return { root, passphrase: passphrase2, created: true, moved: null };
|
|
14259
|
+
}
|
|
14260
|
+
if (root !== null) {
|
|
14261
|
+
const view2 = await tryOpen(statePassphrase(root.seed), path.statePath);
|
|
14262
|
+
if (view2 !== null) {
|
|
14263
|
+
await requireIdentity(view2, root.seed, path.statePath);
|
|
14264
|
+
return { root, passphrase: statePassphrase(root.seed), created: false, moved: null };
|
|
14265
|
+
}
|
|
14266
|
+
}
|
|
14267
|
+
const legacy = await legacyPassphrase(env);
|
|
14268
|
+
const view = legacy === null ? null : await tryOpen(legacy.passphrase, path.statePath);
|
|
14269
|
+
if (legacy === null || view === null) throw new IdentityConflictError(path.statePath);
|
|
14270
|
+
if (root !== null) await requireIdentity(view, root.seed, path.statePath);
|
|
14271
|
+
else {
|
|
14272
|
+
const { secret } = await loadIdentity(view.identity);
|
|
14273
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
14274
|
+
}
|
|
14275
|
+
const passphrase = statePassphrase(root.seed);
|
|
14276
|
+
await saveState(view, passphrase, path.statePath);
|
|
14277
|
+
await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
14278
|
+
return { root, passphrase, created: false, moved: legacy.source };
|
|
14279
|
+
});
|
|
14280
|
+
}
|
|
14281
|
+
async function deleteRootSecret(stateDir) {
|
|
14282
|
+
let seedFile = true;
|
|
14283
|
+
await fs2.unlink(join9(stateDir, SEED_FILE_NAME)).catch((err2) => {
|
|
14284
|
+
if (err2.code !== "ENOENT") throw err2;
|
|
14285
|
+
seedFile = false;
|
|
14286
|
+
});
|
|
14287
|
+
const seed = await keychainDelete(KEYCHAIN_ACCOUNT);
|
|
14288
|
+
const legacy = await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
14289
|
+
return { seedFile, keychain: seed || legacy };
|
|
14290
|
+
}
|
|
14291
|
+
async function probeKeychain() {
|
|
14179
14292
|
const keytar = await loadKeytar();
|
|
14180
|
-
if (keytar === null) return false;
|
|
14293
|
+
if (keytar === null) return { available: false, hasSeed: false };
|
|
14181
14294
|
try {
|
|
14182
|
-
|
|
14295
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
14296
|
+
return { available: true, hasSeed: value !== null && value.length > 0 };
|
|
14183
14297
|
} catch {
|
|
14184
|
-
return false;
|
|
14185
|
-
}
|
|
14186
|
-
}
|
|
14187
|
-
async function promptHiddenPassphrase(prompt) {
|
|
14188
|
-
const stdin = process.stdin;
|
|
14189
|
-
if (!stdin.isTTY) {
|
|
14190
|
-
throw new Error(
|
|
14191
|
-
`Cannot prompt for passphrase: stdin is not a TTY. Set ${ENV_VAR} for non-interactive use.`
|
|
14192
|
-
);
|
|
14298
|
+
return { available: false, hasSeed: false };
|
|
14193
14299
|
}
|
|
14194
|
-
process.stdout.write(prompt);
|
|
14195
|
-
const wasRaw = stdin.isRaw;
|
|
14196
|
-
stdin.setRawMode(true);
|
|
14197
|
-
stdin.resume();
|
|
14198
|
-
stdin.setEncoding("utf8");
|
|
14199
|
-
return new Promise((resolve5, reject) => {
|
|
14200
|
-
let buf = "";
|
|
14201
|
-
const cleanup = () => {
|
|
14202
|
-
stdin.removeListener("data", onData);
|
|
14203
|
-
stdin.setRawMode(wasRaw);
|
|
14204
|
-
stdin.pause();
|
|
14205
|
-
process.stdout.write("\n");
|
|
14206
|
-
};
|
|
14207
|
-
const onData = (chunk) => {
|
|
14208
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
14209
|
-
for (let i = 0; i < text.length; i++) {
|
|
14210
|
-
const ch = text[i];
|
|
14211
|
-
if (ch === "") {
|
|
14212
|
-
cleanup();
|
|
14213
|
-
reject(new Error("cancelled"));
|
|
14214
|
-
return;
|
|
14215
|
-
}
|
|
14216
|
-
if (ch === "" && buf.length === 0) {
|
|
14217
|
-
cleanup();
|
|
14218
|
-
reject(new Error("cancelled"));
|
|
14219
|
-
return;
|
|
14220
|
-
}
|
|
14221
|
-
if (ch === "\r" || ch === "\n") {
|
|
14222
|
-
cleanup();
|
|
14223
|
-
resolve5(buf);
|
|
14224
|
-
return;
|
|
14225
|
-
}
|
|
14226
|
-
if (ch === "\x7F" || ch === "\b") {
|
|
14227
|
-
buf = buf.slice(0, -1);
|
|
14228
|
-
continue;
|
|
14229
|
-
}
|
|
14230
|
-
buf += ch;
|
|
14231
|
-
}
|
|
14232
|
-
};
|
|
14233
|
-
stdin.on("data", onData);
|
|
14234
|
-
});
|
|
14235
14300
|
}
|
|
14236
14301
|
export {
|
|
14237
14302
|
APP_NAME,
|
|
@@ -14239,16 +14304,20 @@ export {
|
|
|
14239
14304
|
BackendError,
|
|
14240
14305
|
DEFAULT_BACKEND_URL,
|
|
14241
14306
|
DEFAULT_PUBLIC_HOST,
|
|
14307
|
+
EPHEMERAL_ENV_VAR,
|
|
14242
14308
|
HandoffBothChannelsFailedError,
|
|
14309
|
+
IdentityConflictError,
|
|
14243
14310
|
KEYCHAIN_ACCOUNT,
|
|
14244
14311
|
KEYCHAIN_SERVICE,
|
|
14245
|
-
KeytarUnavailableError,
|
|
14246
14312
|
LOCK_FILE_NAME,
|
|
14247
14313
|
PASSPHRASE_ENV_VAR,
|
|
14248
14314
|
PUBLIC_HOST_ENV_VAR,
|
|
14249
|
-
|
|
14315
|
+
SEED_ENV_VAR,
|
|
14316
|
+
SEED_FILE_NAME,
|
|
14250
14317
|
STATE_FILE_NAME,
|
|
14251
14318
|
SealApi,
|
|
14319
|
+
SeedInvalidError,
|
|
14320
|
+
StateBusyError,
|
|
14252
14321
|
StateCorrupt,
|
|
14253
14322
|
StateLockedError,
|
|
14254
14323
|
StatePassphraseInvalid,
|
|
@@ -14256,20 +14325,22 @@ export {
|
|
|
14256
14325
|
acquireStateLock,
|
|
14257
14326
|
createEmptyState,
|
|
14258
14327
|
createServer,
|
|
14259
|
-
|
|
14328
|
+
deleteRootSecret,
|
|
14260
14329
|
generateHandle,
|
|
14261
14330
|
generateIdentity,
|
|
14331
|
+
holderMayRun,
|
|
14332
|
+
identityFromSeed,
|
|
14262
14333
|
identityToFields,
|
|
14263
14334
|
isValidHandle,
|
|
14264
14335
|
loadIdentity,
|
|
14265
14336
|
loadState,
|
|
14337
|
+
openAgentState,
|
|
14266
14338
|
performHandoff,
|
|
14267
14339
|
probeKeychain,
|
|
14268
14340
|
probeStateLock,
|
|
14269
|
-
|
|
14341
|
+
readSeed,
|
|
14270
14342
|
recoverPendingOperations,
|
|
14271
14343
|
resolveBackendUrl,
|
|
14272
|
-
resolvePassphraseFromStores,
|
|
14273
14344
|
resolvePublicHost,
|
|
14274
14345
|
resolveStateDir,
|
|
14275
14346
|
saveState,
|
|
@@ -14277,7 +14348,9 @@ export {
|
|
|
14277
14348
|
sealOpen,
|
|
14278
14349
|
sealRevoke,
|
|
14279
14350
|
sealShare,
|
|
14280
|
-
|
|
14351
|
+
statePassphrase,
|
|
14352
|
+
updateState,
|
|
14353
|
+
withStateLock
|
|
14281
14354
|
};
|
|
14282
14355
|
/*! Bundled license information:
|
|
14283
14356
|
|