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/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
- /** `POST /seal/{id}/share` — mint a share link, returns its code. */
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: _SealApi.ownerHeaders(ownerToken, true),
3205
- body: JSON.stringify({ encrypted_blob: encryptedBlobB64 }),
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
- required: ["url"],
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 openShareLink(ctx.client, input.url, { recipientSecret: ctx.identity.secret });
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
- try {
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.3.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: external_exports.string().url().describe(OPEN_INPUT.url.description),
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, { url: args.url, mode: args.mode, dir: args.dir, onProgress: progressReporter(extra) }),
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
  {