sealnet-mcp 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli.cjs +360 -37
- package/dist/cli.js +360 -37
- package/dist/index.cjs +360 -37
- package/dist/index.d.cts +91 -8
- package/dist/index.d.ts +91 -8
- package/dist/index.js +360 -37
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2862,9 +2862,60 @@ var limits_default = {
|
|
|
2862
2862
|
INTAKE_TTL_DEFAULT_SECONDS: 900,
|
|
2863
2863
|
INTAKE_TTL_MAX_SECONDS: 86400,
|
|
2864
2864
|
INTAKE_WAIT_MAX_SECONDS: 25,
|
|
2865
|
-
PAYMENT_WAIT_MAX_SECONDS: 82800
|
|
2865
|
+
PAYMENT_WAIT_MAX_SECONDS: 82800,
|
|
2866
|
+
ADDRESS_PROOF_TTL_SECONDS: 300,
|
|
2867
|
+
INBOX_MAX_ITEMS: 200
|
|
2866
2868
|
}
|
|
2867
2869
|
};
|
|
2870
|
+
var seal_address_v1_default = {
|
|
2871
|
+
version: 1,
|
|
2872
|
+
$comment: "Address of a key: who a seal is for, without a URL (audit/SPEC-ADDRESS.md). Numbers live in limits.json, error codes in error_codes.json.",
|
|
2873
|
+
address: {
|
|
2874
|
+
key: "X25519 public key, 32 bytes",
|
|
2875
|
+
text: "base64url without padding, 43 characters",
|
|
2876
|
+
emoji: "fingerprint_emoji.json over sha256(key)[0..3], the same picture as intake",
|
|
2877
|
+
inbox_hash: "sha256(key), 32 bytes: the server finds an inbox by it"
|
|
2878
|
+
},
|
|
2879
|
+
seed: {
|
|
2880
|
+
env: "SEAL_SEED",
|
|
2881
|
+
text: "base64url without padding of the 32-byte X25519 secret, 43 characters",
|
|
2882
|
+
same_as: [
|
|
2883
|
+
"sealnet-mcp state.identity.x25519_secret_b64",
|
|
2884
|
+
"seal identity.v1.json x25519_secret_b64url",
|
|
2885
|
+
"sealnet (npm) KeyPair.secretKey",
|
|
2886
|
+
"sealnet (PyPI) keygen().secret_key"
|
|
2887
|
+
]
|
|
2888
|
+
},
|
|
2889
|
+
scopes: {
|
|
2890
|
+
share_link: "seal_share",
|
|
2891
|
+
owner_copy: "seal_owner",
|
|
2892
|
+
address_proof: "seal_address_proof"
|
|
2893
|
+
},
|
|
2894
|
+
proof: {
|
|
2895
|
+
header_address: "X-Seal-Address",
|
|
2896
|
+
header_proof: "X-Seal-Proof",
|
|
2897
|
+
proof_text: "<ts>.<token>: ts is the decimal Unix second from the challenge, token base64url of 32 bytes",
|
|
2898
|
+
challenge: "sealed box of the 32-byte token to the address with scope (address_proof, decimal ts): ephemeral_pub(32) || nonce(12) || ciphertext(48), base64url, 123 characters",
|
|
2899
|
+
token: "opaque to clients: unwrap it and send it back"
|
|
2900
|
+
},
|
|
2901
|
+
items: {
|
|
2902
|
+
delivery: {
|
|
2903
|
+
id: "s.<seal_id>.<share_code>",
|
|
2904
|
+
is: "a share link addressed to this key; fragment = its key wrapped to the address (x25519:, targeted_x25519_v1, scope share_link)"
|
|
2905
|
+
}
|
|
2906
|
+
},
|
|
2907
|
+
request_kinds: ["secret", "file", "any"],
|
|
2908
|
+
vectors: [
|
|
2909
|
+
{
|
|
2910
|
+
comment: "generated once by backend_crypto.sealed_box.seal_key_for_recipient; every client must unwrap it",
|
|
2911
|
+
secret: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc",
|
|
2912
|
+
address: "E75P6uryBMf9M1j8nAByGIHRdCeBKCJ-xnTzf3_pe20",
|
|
2913
|
+
ts: 179e7,
|
|
2914
|
+
challenge: "4Oel-UMfgP_1dBhmf13yZiujhPDouCIQ6sSx1nY1qy-vyzFqDqb9jARbJ0e5A8vv4EDELDS5OPqBQMcsmDA-C_lXO95y_CVUyYtSu-1fltsy5ZihOFDp0T-C9Ro",
|
|
2915
|
+
token: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"
|
|
2916
|
+
}
|
|
2917
|
+
]
|
|
2918
|
+
};
|
|
2868
2919
|
var upload_default = {
|
|
2869
2920
|
version: 1,
|
|
2870
2921
|
upload: {
|
|
@@ -3196,13 +3247,24 @@ var SealApi = class _SealApi {
|
|
|
3196
3247
|
});
|
|
3197
3248
|
return { etag: resp.headers.get("ETag") };
|
|
3198
3249
|
}
|
|
3199
|
-
/**
|
|
3250
|
+
/**
|
|
3251
|
+
* `POST /seal/{id}/share` — mint a share link, returns its code. With an
|
|
3252
|
+
* address the link also lands in that key's inbox, in the same request.
|
|
3253
|
+
*/
|
|
3200
3254
|
async createShareLink(sealId, ownerToken, encryptedBlobB64, opts = {}) {
|
|
3255
|
+
const headers = _SealApi.ownerHeaders(ownerToken, true);
|
|
3256
|
+
const body = { encrypted_blob: encryptedBlobB64 };
|
|
3257
|
+
if ("to" in opts) {
|
|
3258
|
+
body.to = opts.to;
|
|
3259
|
+
body.fragment = opts.fragment;
|
|
3260
|
+
if (opts.label) body.label = opts.label;
|
|
3261
|
+
if (opts.sender) Object.assign(headers, proofHeaders(opts.sender));
|
|
3262
|
+
}
|
|
3201
3263
|
const resp = await this.request(`/seal/${encode(sealId)}/share`, {
|
|
3202
3264
|
what: `POST /seal/${sealId}/share`,
|
|
3203
3265
|
method: "POST",
|
|
3204
|
-
headers
|
|
3205
|
-
body: JSON.stringify(
|
|
3266
|
+
headers,
|
|
3267
|
+
body: JSON.stringify(body),
|
|
3206
3268
|
signal: opts.signal
|
|
3207
3269
|
});
|
|
3208
3270
|
const data = await resp.json();
|
|
@@ -3385,8 +3447,57 @@ var SealApi = class _SealApi {
|
|
|
3385
3447
|
});
|
|
3386
3448
|
return await resp.json();
|
|
3387
3449
|
}
|
|
3450
|
+
// ---------------------------------------------------------------------------
|
|
3451
|
+
// Inbox (SPEC-ADDRESS F2): the share links addressed to a key
|
|
3452
|
+
// ---------------------------------------------------------------------------
|
|
3453
|
+
/** `POST /intake/proof` — a challenge sealed to `address`. */
|
|
3454
|
+
async requestAddressProof(address, opts = {}) {
|
|
3455
|
+
const resp = await this.request("/intake/proof", {
|
|
3456
|
+
what: "POST /intake/proof",
|
|
3457
|
+
method: "POST",
|
|
3458
|
+
headers: { "Content-Type": "application/json" },
|
|
3459
|
+
body: JSON.stringify({ address }),
|
|
3460
|
+
signal: opts.signal
|
|
3461
|
+
});
|
|
3462
|
+
return await resp.json();
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* `GET /intake?to=` — every live item. `waitSeconds` holds the answer
|
|
3466
|
+
* while the inbox is empty, or, with `seen`, while its version is still
|
|
3467
|
+
* `seen`; one call waits at most `INTAKE_WAIT_MAX_SECONDS`, longer waits
|
|
3468
|
+
* are the caller's loop.
|
|
3469
|
+
*/
|
|
3470
|
+
async listInbox(proof, opts = {}) {
|
|
3471
|
+
const params = new URLSearchParams({ to: proof.address });
|
|
3472
|
+
if (opts.waitSeconds) {
|
|
3473
|
+
params.set("wait", String(Math.min(opts.waitSeconds, limits_default.limits.INTAKE_WAIT_MAX_SECONDS)));
|
|
3474
|
+
}
|
|
3475
|
+
if (opts.seen) params.set("seen", opts.seen);
|
|
3476
|
+
const resp = await this.request(`/intake?${params.toString()}`, {
|
|
3477
|
+
what: "GET /intake",
|
|
3478
|
+
headers: proofHeaders(proof),
|
|
3479
|
+
signal: opts.signal
|
|
3480
|
+
});
|
|
3481
|
+
return await resp.json();
|
|
3482
|
+
}
|
|
3483
|
+
/** `DELETE /intake?to=&item=` — take one item out; the link keeps working. */
|
|
3484
|
+
async dismissInboxItem(itemId, proof, opts = {}) {
|
|
3485
|
+
const params = new URLSearchParams({ to: proof.address, item: itemId });
|
|
3486
|
+
await this.request(`/intake?${params.toString()}`, {
|
|
3487
|
+
what: "DELETE /intake",
|
|
3488
|
+
method: "DELETE",
|
|
3489
|
+
headers: proofHeaders(proof),
|
|
3490
|
+
signal: opts.signal
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3388
3493
|
};
|
|
3389
3494
|
var NOT_READY_DEFAULT_RETRY_AFTER_S = 3;
|
|
3495
|
+
function proofHeaders(proof) {
|
|
3496
|
+
return {
|
|
3497
|
+
[seal_address_v1_default.proof.header_address]: proof.address,
|
|
3498
|
+
[seal_address_v1_default.proof.header_proof]: proof.proof
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3390
3501
|
function abortableSleep(ms, signal) {
|
|
3391
3502
|
return new Promise((resolve5, reject) => {
|
|
3392
3503
|
if (signal?.aborted) {
|
|
@@ -6151,6 +6262,7 @@ function parseUrlType(pathname) {
|
|
|
6151
6262
|
var TARGETED_PREFIX_V1 = "x25519:";
|
|
6152
6263
|
var TARGETED_PREFIX_V2_RESERVED = "x25519v2:";
|
|
6153
6264
|
var SCOPE_TYPE = "seal_share";
|
|
6265
|
+
var ADDRESS_PROOF_SCOPE = "seal_address_proof";
|
|
6154
6266
|
var X25519_KEY_BYTES = 32;
|
|
6155
6267
|
var LINK_KEY_BYTES2 = 32;
|
|
6156
6268
|
var PACKED_BYTES_V1 = 92;
|
|
@@ -6257,6 +6369,11 @@ async function unwrapTargetedFragment(fragment, recipientSecret, sealId) {
|
|
|
6257
6369
|
}
|
|
6258
6370
|
return linkKey;
|
|
6259
6371
|
}
|
|
6372
|
+
async function proveAddress(secret, challenge, ts) {
|
|
6373
|
+
const wasm3 = await ensureWasm();
|
|
6374
|
+
const token = wasm3.wasm_sealbox_unwrap(base64urlToBytes(challenge), secret, ADDRESS_PROOF_SCOPE, String(ts));
|
|
6375
|
+
return `${ts}.${bytesToBase64url(token)}`;
|
|
6376
|
+
}
|
|
6260
6377
|
var fingerprint_emoji_default = {
|
|
6261
6378
|
version: 1,
|
|
6262
6379
|
name: "FINGERPRINT_EMOJI",
|
|
@@ -6716,6 +6833,36 @@ async function waitForIntake(api, intakeId, opts = {}) {
|
|
|
6716
6833
|
if (view.status === "fulfilled" && view.share_path) return view.share_path;
|
|
6717
6834
|
}
|
|
6718
6835
|
}
|
|
6836
|
+
var PROOF_MARGIN_MS = 3e4;
|
|
6837
|
+
var proofs = /* @__PURE__ */ new Map();
|
|
6838
|
+
async function addressProof(api, secret, opts = {}) {
|
|
6839
|
+
const address = bytesToBase64url(await recipientPublicKey(secret));
|
|
6840
|
+
const key = `${api.baseUrl} ${address}`;
|
|
6841
|
+
const kept = proofs.get(key);
|
|
6842
|
+
if (!opts.fresh && kept && kept.until > Date.now()) return kept.proof;
|
|
6843
|
+
const challenge = await api.requestAddressProof(address, opts);
|
|
6844
|
+
const proof = { address, proof: await proveAddress(secret, challenge.challenge, challenge.ts) };
|
|
6845
|
+
proofs.set(key, { proof, until: Date.parse(challenge.expires_at) - PROOF_MARGIN_MS });
|
|
6846
|
+
return proof;
|
|
6847
|
+
}
|
|
6848
|
+
async function withAddressProof(api, secret, signal, call) {
|
|
6849
|
+
const request = signal ? { signal } : {};
|
|
6850
|
+
try {
|
|
6851
|
+
return await call(await addressProof(api, secret, request));
|
|
6852
|
+
} catch (err2) {
|
|
6853
|
+
if (!(err2 instanceof BackendError && err2.appCode === "intake.proof_required")) throw err2;
|
|
6854
|
+
return call(await addressProof(api, secret, { ...request, fresh: true }));
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
async function readInbox(api, secret, opts = {}) {
|
|
6858
|
+
return withAddressProof(api, secret, opts.signal, (proof) => api.listInbox(proof, opts));
|
|
6859
|
+
}
|
|
6860
|
+
async function removeFromInbox(api, secret, itemId, opts = {}) {
|
|
6861
|
+
return withAddressProof(api, secret, opts.signal, (proof) => api.dismissInboxItem(itemId, proof, opts));
|
|
6862
|
+
}
|
|
6863
|
+
function inboxItemUrl(site, item) {
|
|
6864
|
+
return `${site.replace(/\/+$/, "")}${item.path}#${item.fragment}`;
|
|
6865
|
+
}
|
|
6719
6866
|
async function describeFile(file, fileKeys) {
|
|
6720
6867
|
const keyB64 = file.sha256_hex ? fileKeys[file.sha256_hex] : void 0;
|
|
6721
6868
|
const base = { id: file.id, size: file.size, sha256Hex: file.sha256_hex };
|
|
@@ -11636,7 +11783,7 @@ var seal_mcp_v1_default = {
|
|
|
11636
11783
|
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`."
|
|
11637
11784
|
},
|
|
11638
11785
|
server_instructions: {
|
|
11639
|
-
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.",
|
|
11786
|
+
text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files). Files sent to your key arrive in seal_inbox; open them with seal_open item. Never read or print a SEAL handoff file.",
|
|
11640
11787
|
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."
|
|
11641
11788
|
},
|
|
11642
11789
|
tools: {
|
|
@@ -11704,7 +11851,8 @@ var seal_mcp_v1_default = {
|
|
|
11704
11851
|
share_url: { type: "string", format: "uri", description: "`targeted_x25519_v1` link: the wrapped key is in the fragment, useless without the recipient's X25519 secret; there is no separate password." },
|
|
11705
11852
|
expires_at: { type: "string", format: "date-time" },
|
|
11706
11853
|
max_reads: { type: "integer", const: 1 },
|
|
11707
|
-
mode: { type: "string", const: "forward" }
|
|
11854
|
+
mode: { type: "string", const: "forward" },
|
|
11855
|
+
delivered: { type: "boolean", description: "The link also lies in the recipient's inbox (SPEC-ADDRESS F2): the server keeps its wrapped key in the link's row, and the holder of the key finds it with seal_inbox without being handed the URL." }
|
|
11708
11856
|
},
|
|
11709
11857
|
policy: {
|
|
11710
11858
|
max_expire_minutes: 30,
|
|
@@ -11772,9 +11920,11 @@ var seal_mcp_v1_default = {
|
|
|
11772
11920
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
11773
11921
|
annotations_note: "Set on the tool, not per mode: `inline` and `file` consume an open, so the tool as a whole is not read-only.",
|
|
11774
11922
|
input: {
|
|
11775
|
-
|
|
11923
|
+
one_of: ["url", "item"],
|
|
11924
|
+
one_of_note: "Exactly one of `url` and `item`; `server.ts` takes both as optional and the handler refuses anything else with `url_invalid`.",
|
|
11776
11925
|
properties: {
|
|
11777
11926
|
url: { type: "string", format: "uri", description: "Full SEAL share URL including fragment." },
|
|
11927
|
+
item: { type: "string", pattern: "^in_", description: "An inbox handle from seal_inbox (`in_\u2026`), instead of a URL." },
|
|
11778
11928
|
mode: { type: "string", enum: ["metadata", "inline", "file"], default: "metadata", description: "`metadata` reads nothing; `file` saves to disk; `inline` returns small text." },
|
|
11779
11929
|
dir: { type: "string", description: "mode=file: directory to save into; default ~/Downloads, created if missing. An existing file is never overwritten: the new one becomes `name (1).ext`." }
|
|
11780
11930
|
}
|
|
@@ -11831,6 +11981,40 @@ var seal_mcp_v1_default = {
|
|
|
11831
11981
|
_binary_default: "inline_blocked.binary"
|
|
11832
11982
|
}
|
|
11833
11983
|
},
|
|
11984
|
+
seal_inbox: {
|
|
11985
|
+
title: "Check the inbox",
|
|
11986
|
+
description: "Seals other agents or people sent to your key (your address). Returns handles; open one with seal_open item=<handle>. `wait`: seconds to wait for something new, 0 to 300.",
|
|
11987
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
11988
|
+
input: {
|
|
11989
|
+
properties: {
|
|
11990
|
+
wait: { type: "integer", minimum: 0, maximum: 300, default: 0, description: "Seconds to wait for an item this server has not seen yet; 0 returns the inbox now." }
|
|
11991
|
+
}
|
|
11992
|
+
},
|
|
11993
|
+
flow: "`GET /intake?to=<this server's address>` under the proof of the key (seal_address.v1.json proof), cached until 30 s before it expires. The first call in a process returns the list at once; later ones long-poll by 25 s with `seen` = the version of the last answer until an id this process has not seen arrives or `wait` runs out. Handles, rows and the version live in process memory.",
|
|
11994
|
+
output: {
|
|
11995
|
+
type: "object",
|
|
11996
|
+
required: ["address_emoji", "items"],
|
|
11997
|
+
properties: {
|
|
11998
|
+
address_emoji: { type: "string", description: "Three emoji of this server's address (`fingerprint_emoji.json`)." },
|
|
11999
|
+
items: {
|
|
12000
|
+
type: "array",
|
|
12001
|
+
items: {
|
|
12002
|
+
type: "object",
|
|
12003
|
+
required: ["handle", "from", "label", "received_at", "expires_at"],
|
|
12004
|
+
additionalProperties: false,
|
|
12005
|
+
properties: {
|
|
12006
|
+
handle: { type: "string", pattern: "^in_", description: "`in_` + the inbox item id; pass as seal_open `item`." },
|
|
12007
|
+
from: { type: ["string", "null"], description: "Three emoji of the sender's address when the server verified it, else null." },
|
|
12008
|
+
label: { type: ["string", "null"], description: "What the sender called itself (an agent's name, `sealnet`), in the clear: never content." },
|
|
12009
|
+
received_at: { type: "string", format: "date-time" },
|
|
12010
|
+
expires_at: { type: "string", format: "date-time" }
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
},
|
|
12015
|
+
MUST_NOT_contain: ["path", "fragment", "url", "seal_id", "share_code"]
|
|
12016
|
+
}
|
|
12017
|
+
},
|
|
11834
12018
|
seal_list: {
|
|
11835
12019
|
title: "List SEALs created via THIS MCP server",
|
|
11836
12020
|
description: "Seals you created through this server: opaque handles with status and expiry. Seals made with the CLI or the desktop client live elsewhere and are not listed.",
|
|
@@ -12154,6 +12338,10 @@ var seal_mcp_v1_default = {
|
|
|
12154
12338
|
share_blob_corrupt: "Share blob decoded but a file_key is not valid base64url or has the wrong length (32 bytes expected). Backend bug or share-blob tampering.",
|
|
12155
12339
|
share_blob_failed: "`encryptShareBlob` threw during seal_share mint (Web Crypto failure or input shape bug). Caller-facing in seal_share path only.",
|
|
12156
12340
|
share_link_failed: "POST /seal/{id}/share returned an error (network, 5xx, owner_token mismatch).",
|
|
12341
|
+
inbox_proof_failed: "seal_inbox / seal_open item / seal_share forward: the server refused the proof of this server's key (`intake.proof_required`) even after one fresh challenge.",
|
|
12342
|
+
inbox_full: "seal_share forward: the recipient's inbox holds `limits.json::INBOX_MAX_ITEMS` live items from other senders (`intake.inbox_full`). The seal is revoked; retry later.",
|
|
12343
|
+
inbox_item_gone: "seal_open item: the handle is not in this process's inbox and not in a fresh read of it \u2014 the seal or link was revoked or expired, or the item was removed.",
|
|
12344
|
+
inbox_failed: "seal_inbox / seal_open item: reading or cleaning the inbox failed for a reason other than the proof (network, 5xx, rate limit); the message has the status. Safe to retry.",
|
|
12157
12345
|
targeted_fragment_invalid: "seal_open of `x25519:` URL: fragment is malformed base64url, wrong packed length, or AEAD-failed for a non-secret reason. Distinct from `targeted_decrypt_failed` which is the wrong-recipient case.",
|
|
12158
12346
|
targeted_decrypt_failed: "seal_open of `x25519:` URL: AEAD verification failed under THIS device's identity. The URL was not addressed to this MCP's pubkey (or was tampered, or seal_id from path-segment was substituted). Recipient should ask the sender to re-mint with `--to <our pubkey>`.",
|
|
12159
12347
|
auth_required: "Backend returned 401/403 because the seal has OTP / NDA / login gating; interactive auth is not supported in MCP. Open the URL in a browser.",
|
|
@@ -12763,6 +12951,72 @@ var ToolError = class extends Error {
|
|
|
12763
12951
|
}
|
|
12764
12952
|
};
|
|
12765
12953
|
|
|
12954
|
+
// src/tools/inbox.ts
|
|
12955
|
+
var HANDLE_PREFIX2 = "in_";
|
|
12956
|
+
function inboxError(err2) {
|
|
12957
|
+
if (!(err2 instanceof BackendError)) return err2;
|
|
12958
|
+
if (err2.appCode === "intake.proof_required") {
|
|
12959
|
+
return new ToolError("inbox_proof_failed", `the server refused the proof of this server's key: ${err2.message}`, err2);
|
|
12960
|
+
}
|
|
12961
|
+
return new ToolError("inbox_failed", `the inbox failed (${err2.status}): ${err2.message}`, err2);
|
|
12962
|
+
}
|
|
12963
|
+
function memoryOf(ctx) {
|
|
12964
|
+
if (!ctx.inbox) throw new ToolError("internal_inconsistency", "this server keeps no inbox memory");
|
|
12965
|
+
return ctx.inbox;
|
|
12966
|
+
}
|
|
12967
|
+
async function sealInbox(ctx, input = {}) {
|
|
12968
|
+
const memory = memoryOf(ctx);
|
|
12969
|
+
const deadline = Date.now() + (memory.version === null ? 0 : (input.wait ?? 0) * 1e3);
|
|
12970
|
+
let seen = memory.version ?? void 0;
|
|
12971
|
+
let inbox;
|
|
12972
|
+
for (; ; ) {
|
|
12973
|
+
const left = Math.ceil((deadline - Date.now()) / 1e3);
|
|
12974
|
+
try {
|
|
12975
|
+
inbox = await readInbox(ctx.client, ctx.identity.secret, left > 0 ? { waitSeconds: left, seen } : {});
|
|
12976
|
+
} catch (err2) {
|
|
12977
|
+
throw inboxError(err2);
|
|
12978
|
+
}
|
|
12979
|
+
const arrived = inbox.items.some((item) => !memory.items.has(HANDLE_PREFIX2 + item.id));
|
|
12980
|
+
if (left <= 0 || arrived || Date.now() >= deadline) break;
|
|
12981
|
+
seen = inbox.version;
|
|
12982
|
+
}
|
|
12983
|
+
memory.items.clear();
|
|
12984
|
+
for (const item of inbox.items) memory.items.set(HANDLE_PREFIX2 + item.id, item);
|
|
12985
|
+
memory.version = inbox.version;
|
|
12986
|
+
return {
|
|
12987
|
+
addressEmoji: keyEmoji(ctx.identity.public),
|
|
12988
|
+
items: inbox.items.map((item) => ({
|
|
12989
|
+
handle: HANDLE_PREFIX2 + item.id,
|
|
12990
|
+
from: item.sender === null ? null : keyEmoji(base64urlToBytes(item.sender)),
|
|
12991
|
+
label: item.label,
|
|
12992
|
+
receivedAt: item.at,
|
|
12993
|
+
expiresAt: item.expires_at
|
|
12994
|
+
}))
|
|
12995
|
+
};
|
|
12996
|
+
}
|
|
12997
|
+
async function inboxItem(ctx, handle) {
|
|
12998
|
+
const memory = memoryOf(ctx);
|
|
12999
|
+
if (!memory.items.has(handle)) await sealInbox(ctx, {});
|
|
13000
|
+
const item = memory.items.get(handle);
|
|
13001
|
+
if (!item) {
|
|
13002
|
+
throw new ToolError(
|
|
13003
|
+
"inbox_item_gone",
|
|
13004
|
+
`${handle} is not in the inbox: the seal or link was revoked or expired, or the item was removed`
|
|
13005
|
+
);
|
|
13006
|
+
}
|
|
13007
|
+
return item;
|
|
13008
|
+
}
|
|
13009
|
+
async function forgetInboxItem(ctx, handle) {
|
|
13010
|
+
const item = memoryOf(ctx).items.get(handle);
|
|
13011
|
+
if (!item) return;
|
|
13012
|
+
try {
|
|
13013
|
+
await removeFromInbox(ctx.client, ctx.identity.secret, item.id);
|
|
13014
|
+
} catch (err2) {
|
|
13015
|
+
throw inboxError(err2);
|
|
13016
|
+
}
|
|
13017
|
+
memoryOf(ctx).items.delete(handle);
|
|
13018
|
+
}
|
|
13019
|
+
|
|
12766
13020
|
// src/tools/list.ts
|
|
12767
13021
|
var SINGLE_FILE_COUNT = 1;
|
|
12768
13022
|
async function sealList(ctx, input = {}) {
|
|
@@ -12854,11 +13108,28 @@ async function download(ctx, sealId, file) {
|
|
|
12854
13108
|
return resp.body;
|
|
12855
13109
|
}
|
|
12856
13110
|
async function sealOpen(ctx, input) {
|
|
13111
|
+
if (input.url === void 0 === (input.item === void 0)) {
|
|
13112
|
+
throw new ToolError("url_invalid", "give exactly one of `url` (a SEAL link) and `item` (an inbox handle from seal_inbox)");
|
|
13113
|
+
}
|
|
13114
|
+
const recipient = { recipientSecret: ctx.identity.secret };
|
|
13115
|
+
if (input.url !== void 0) return openSeal(ctx, input, () => openShareLink(ctx.client, input.url, recipient));
|
|
13116
|
+
const handle = input.item;
|
|
13117
|
+
const item = await inboxItem(ctx, handle);
|
|
13118
|
+
try {
|
|
13119
|
+
return await openSeal(ctx, input, () => openShareLink(ctx.client, inboxItemUrl(resolvePublicHost(), item), recipient));
|
|
13120
|
+
} catch (err2) {
|
|
13121
|
+
if (err2 instanceof ToolError && (err2.code === "seal_revoked" || err2.code === "share_link_revoked")) {
|
|
13122
|
+
await forgetInboxItem(ctx, handle).catch(() => void 0);
|
|
13123
|
+
}
|
|
13124
|
+
throw err2;
|
|
13125
|
+
}
|
|
13126
|
+
}
|
|
13127
|
+
async function openSeal(ctx, input, read) {
|
|
12857
13128
|
const mode = input.mode ?? "metadata";
|
|
12858
13129
|
const handle = generateHandle();
|
|
12859
13130
|
let seal;
|
|
12860
13131
|
try {
|
|
12861
|
-
seal = await
|
|
13132
|
+
seal = await read();
|
|
12862
13133
|
} catch (err2) {
|
|
12863
13134
|
throw asToolError(err2);
|
|
12864
13135
|
}
|
|
@@ -13206,7 +13477,9 @@ var limits_default2 = {
|
|
|
13206
13477
|
INTAKE_TTL_DEFAULT_SECONDS: 900,
|
|
13207
13478
|
INTAKE_TTL_MAX_SECONDS: 86400,
|
|
13208
13479
|
INTAKE_WAIT_MAX_SECONDS: 25,
|
|
13209
|
-
PAYMENT_WAIT_MAX_SECONDS: 82800
|
|
13480
|
+
PAYMENT_WAIT_MAX_SECONDS: 82800,
|
|
13481
|
+
ADDRESS_PROOF_TTL_SECONDS: 300,
|
|
13482
|
+
INBOX_MAX_ITEMS: 200
|
|
13210
13483
|
}
|
|
13211
13484
|
};
|
|
13212
13485
|
|
|
@@ -13724,17 +13997,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13724
13997
|
);
|
|
13725
13998
|
}
|
|
13726
13999
|
const shareBlobB64 = bytesToBase64url(shareBlobBytes);
|
|
13727
|
-
let shareCode;
|
|
13728
|
-
try {
|
|
13729
|
-
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13730
|
-
} catch (err2) {
|
|
13731
|
-
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13732
|
-
throw new ToolError(
|
|
13733
|
-
"share_link_failed",
|
|
13734
|
-
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13735
|
-
err2
|
|
13736
|
-
);
|
|
13737
|
-
}
|
|
13738
14000
|
let fragment;
|
|
13739
14001
|
if (mode === "forward") {
|
|
13740
14002
|
if (recipientPub === null) {
|
|
@@ -13764,6 +14026,29 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13764
14026
|
} else {
|
|
13765
14027
|
fragment = bytesToBase64url(shareLinkKey);
|
|
13766
14028
|
}
|
|
14029
|
+
if (mode === "forward") await journalDelivering(ctx, handle, op);
|
|
14030
|
+
let shareCode;
|
|
14031
|
+
try {
|
|
14032
|
+
shareCode = mode === "forward" && recipientPub !== null ? await withAddressProof(
|
|
14033
|
+
ctx.client,
|
|
14034
|
+
ctx.identity.secret,
|
|
14035
|
+
void 0,
|
|
14036
|
+
(sender) => ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64, {
|
|
14037
|
+
to: bytesToBase64url(recipientPub),
|
|
14038
|
+
fragment,
|
|
14039
|
+
label: deps.label?.slice(0, 80),
|
|
14040
|
+
sender
|
|
14041
|
+
})
|
|
14042
|
+
) : await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
14043
|
+
} catch (err2) {
|
|
14044
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
14045
|
+
const appCode = err2 instanceof BackendError ? err2.appCode : void 0;
|
|
14046
|
+
throw new ToolError(
|
|
14047
|
+
appCode === "intake.inbox_full" ? "inbox_full" : appCode === "intake.proof_required" ? "inbox_proof_failed" : "share_link_failed",
|
|
14048
|
+
`POST /seal/${uploaded.sealId}/share failed: ${messageOf3(err2)} (${outcome})`,
|
|
14049
|
+
err2
|
|
14050
|
+
);
|
|
14051
|
+
}
|
|
13767
14052
|
const host = resolvePublicHost(deps.publicHost);
|
|
13768
14053
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13769
14054
|
if (mode === "forward") {
|
|
@@ -13780,21 +14065,12 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13780
14065
|
shareUrl,
|
|
13781
14066
|
expiresAt,
|
|
13782
14067
|
maxReads: FORWARD_MAX_READS_FORCED,
|
|
13783
|
-
mode: "forward"
|
|
14068
|
+
mode: "forward",
|
|
14069
|
+
delivered: true
|
|
13784
14070
|
};
|
|
13785
14071
|
}
|
|
13786
|
-
op.step = "delivering";
|
|
13787
14072
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13788
|
-
|
|
13789
|
-
await journal(ctx, handle, op);
|
|
13790
|
-
} catch (err2) {
|
|
13791
|
-
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13792
|
-
throw new ToolError(
|
|
13793
|
-
"state_persist_failed",
|
|
13794
|
-
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13795
|
-
err2
|
|
13796
|
-
);
|
|
13797
|
-
}
|
|
14073
|
+
await journalDelivering(ctx, handle, op);
|
|
13798
14074
|
let handoff;
|
|
13799
14075
|
try {
|
|
13800
14076
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
@@ -13829,6 +14105,19 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13829
14105
|
function messageOf3(err2) {
|
|
13830
14106
|
return err2 instanceof Error ? err2.message : String(err2);
|
|
13831
14107
|
}
|
|
14108
|
+
async function journalDelivering(ctx, handle, op) {
|
|
14109
|
+
op.step = "delivering";
|
|
14110
|
+
try {
|
|
14111
|
+
await journal(ctx, handle, op);
|
|
14112
|
+
} catch (err2) {
|
|
14113
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
14114
|
+
throw new ToolError(
|
|
14115
|
+
"state_persist_failed",
|
|
14116
|
+
`could not journal the delivery attempt before the link could reach anyone: ${messageOf3(err2)} (${outcome})`,
|
|
14117
|
+
err2
|
|
14118
|
+
);
|
|
14119
|
+
}
|
|
14120
|
+
}
|
|
13832
14121
|
async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, session }) {
|
|
13833
14122
|
const tier = prepared.tier;
|
|
13834
14123
|
const sealId = op.seal_id;
|
|
@@ -13920,7 +14209,8 @@ function shareOutputToWire(out) {
|
|
|
13920
14209
|
share_url: out.shareUrl,
|
|
13921
14210
|
expires_at: out.expiresAt,
|
|
13922
14211
|
max_reads: out.maxReads,
|
|
13923
|
-
mode: out.mode
|
|
14212
|
+
mode: out.mode,
|
|
14213
|
+
delivered: out.delivered
|
|
13924
14214
|
};
|
|
13925
14215
|
}
|
|
13926
14216
|
const wire = {
|
|
@@ -13987,6 +14277,18 @@ function openOutputToWire(out) {
|
|
|
13987
14277
|
expires_at: out.expiresAt
|
|
13988
14278
|
};
|
|
13989
14279
|
}
|
|
14280
|
+
function inboxOutputToWire(out) {
|
|
14281
|
+
return {
|
|
14282
|
+
address_emoji: out.addressEmoji,
|
|
14283
|
+
items: out.items.map((item) => ({
|
|
14284
|
+
handle: item.handle,
|
|
14285
|
+
from: item.from,
|
|
14286
|
+
label: item.label,
|
|
14287
|
+
received_at: item.receivedAt,
|
|
14288
|
+
expires_at: item.expiresAt
|
|
14289
|
+
}))
|
|
14290
|
+
};
|
|
14291
|
+
}
|
|
13990
14292
|
function listOutputToWire(out) {
|
|
13991
14293
|
return {
|
|
13992
14294
|
entries: out.entries.map((entry) => ({
|
|
@@ -14017,7 +14319,7 @@ function proFileGetOutputToWire(out) {
|
|
|
14017
14319
|
}
|
|
14018
14320
|
|
|
14019
14321
|
// src/server.ts
|
|
14020
|
-
var SERVER_VERSION = "0.
|
|
14322
|
+
var SERVER_VERSION = "0.4.0";
|
|
14021
14323
|
var toolMeta = (name) => {
|
|
14022
14324
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
14023
14325
|
return { title, description, annotations };
|
|
@@ -14025,6 +14327,7 @@ var toolMeta = (name) => {
|
|
|
14025
14327
|
var SHARE_INPUT = seal_mcp_v1_default.tools.seal_share.input.properties;
|
|
14026
14328
|
var REQUEST_INPUT = seal_mcp_v1_default.tools.seal_request.input.properties;
|
|
14027
14329
|
var OPEN_INPUT = seal_mcp_v1_default.tools.seal_open.input.properties;
|
|
14330
|
+
var INBOX_INPUT = seal_mcp_v1_default.tools.seal_inbox.input.properties;
|
|
14028
14331
|
var PRO_SECRET_USE = seal_mcp_v1_default.pro_tools.seal_pro_secret_use;
|
|
14029
14332
|
var PRO_SECRET_REQUEST = seal_mcp_v1_default.pro_tools.seal_pro_secret_request;
|
|
14030
14333
|
var PRO_FILE_GET = seal_mcp_v1_default.pro_tools.seal_pro_file_get;
|
|
@@ -14069,7 +14372,8 @@ async function createServer(options) {
|
|
|
14069
14372
|
update,
|
|
14070
14373
|
ephemeral: disk === null,
|
|
14071
14374
|
requests: /* @__PURE__ */ new Map(),
|
|
14072
|
-
payments: /* @__PURE__ */ new Map()
|
|
14375
|
+
payments: /* @__PURE__ */ new Map(),
|
|
14376
|
+
inbox: { items: /* @__PURE__ */ new Map(), version: null }
|
|
14073
14377
|
};
|
|
14074
14378
|
const mcp = new McpServer(
|
|
14075
14379
|
{ name: "sealnet-mcp", version: SERVER_VERSION },
|
|
@@ -14116,6 +14420,7 @@ async function createServer(options) {
|
|
|
14116
14420
|
},
|
|
14117
14421
|
{
|
|
14118
14422
|
publicHost,
|
|
14423
|
+
label: client2.getClientVersion()?.name,
|
|
14119
14424
|
handoff: disableClipboard ? { disableClipboard: true } : void 0,
|
|
14120
14425
|
elicitUrl: urlMode ? async (params) => (await client2.elicitInput({ mode: "url", ...params })).action : void 0,
|
|
14121
14426
|
elicitationDone: urlMode ? (id) => client2.createElicitationCompletionNotifier(id)() : void 0
|
|
@@ -14169,19 +14474,37 @@ async function createServer(options) {
|
|
|
14169
14474
|
{
|
|
14170
14475
|
...toolMeta("seal_open"),
|
|
14171
14476
|
inputSchema: {
|
|
14172
|
-
url
|
|
14477
|
+
// Exactly one of `url` and `item`: the handler refuses anything else (contract `one_of`).
|
|
14478
|
+
url: external_exports.string().url().optional().describe(OPEN_INPUT.url.description),
|
|
14479
|
+
item: external_exports.string().regex(/^in_/).optional().describe(OPEN_INPUT.item.description),
|
|
14173
14480
|
mode: external_exports.enum(["metadata", "inline", "file"]).default("metadata").describe(OPEN_INPUT.mode.description),
|
|
14174
14481
|
dir: external_exports.string().optional().describe(OPEN_INPUT.dir.description)
|
|
14175
14482
|
}
|
|
14176
14483
|
},
|
|
14177
14484
|
async (args, extra) => {
|
|
14178
14485
|
const result = await wrapTool(
|
|
14179
|
-
() => sealOpen(ctx, {
|
|
14486
|
+
() => sealOpen(ctx, {
|
|
14487
|
+
url: args.url,
|
|
14488
|
+
item: args.item,
|
|
14489
|
+
mode: args.mode,
|
|
14490
|
+
dir: args.dir,
|
|
14491
|
+
onProgress: progressReporter(extra)
|
|
14492
|
+
}),
|
|
14180
14493
|
openOutputToWire
|
|
14181
14494
|
);
|
|
14182
14495
|
return result;
|
|
14183
14496
|
}
|
|
14184
14497
|
);
|
|
14498
|
+
mcp.registerTool(
|
|
14499
|
+
"seal_inbox",
|
|
14500
|
+
{
|
|
14501
|
+
...toolMeta("seal_inbox"),
|
|
14502
|
+
inputSchema: {
|
|
14503
|
+
wait: external_exports.number().int().min(0).max(300).default(0).describe(INBOX_INPUT.wait.description)
|
|
14504
|
+
}
|
|
14505
|
+
},
|
|
14506
|
+
async (args) => wrapTool(() => sealInbox(ctx, { wait: args.wait }), inboxOutputToWire)
|
|
14507
|
+
);
|
|
14185
14508
|
mcp.registerTool(
|
|
14186
14509
|
"seal_list",
|
|
14187
14510
|
{
|