sealnet-mcp 0.2.4 → 0.2.5

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/index.d.cts CHANGED
@@ -1,108 +1,457 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { WorkloadCredentials } from '@seal/crypto/node';
4
- import { S } from './open-DUqF6AyG.js';
5
- export { B as BackendError, S as SealApi, G as SealApiOptions, a0 as SealMetadata } from './open-DUqF6AyG.js';
6
3
 
7
4
  /**
8
- * MCP server bootstrap — wires the five tools (`seal_share`,
9
- * `seal_request`, `seal_open`, `seal_list`, `seal_revoke`) to a
10
- * stdio-transport `McpServer` instance, and `seal_pro_secret_use`,
11
- * `seal_pro_secret_request` and `seal_pro_file_get` when SEAL Pro workload
12
- * credentials exist (contract `pro_tools`).
13
- *
14
- * The bootstrap is intentionally factored as a `createServer()`
15
- * factory that returns an awaitable handle, NOT a hard-coded
16
- * `main()` entry. That shape:
17
- * - lets `cli.ts` configure passphrase delivery, state path,
18
- * backend base URL, etc. before launching;
19
- * - lets test code spawn an in-process MCP without spawning a
20
- * subprocess;
21
- * - keeps the "where do creds come from?" decision out of the
22
- * pure tool implementations.
23
- *
24
- * Tool `title`, `description` and `annotations` come from
25
- * `shared/contracts/seal_mcp.v1.json` (contract first, §6.2), and so do
26
- * the descriptions of the inputs the contract describes; the zod input
27
- * schemas live here.
5
+ * The `/seal/*` HTTP surface — one implementation for every runtime.
6
+ *
7
+ * Everything the protocol does with bytes (AEON framing, streaming upload,
8
+ * download + decrypt, blobs, key store) already lives in this package. What
9
+ * did not was the thin layer around it: the routes, the wire shapes and the
10
+ * error classification of the SEAL backend's non-upload endpoints. That layer
11
+ * existed six times — the Slack bot's `seal/backend.ts`, the MCP server's
12
+ * `client.ts` (a line-for-line subset of it), the SEAL web's `seal_client.ts`
13
+ * plus a second axios stack in `lib/api.ts`, fifteen inline `fetch` calls
14
+ * across the web's features, and the SDK's `anonymous.ts`. One contract,
15
+ * six readings of it, and every fix landing in one of them.
16
+ *
17
+ * This module is that layer, once. It is deliberately NOT a runtime: it
18
+ * takes a `fetch` and a `baseUrl` and hands back parsed wire objects or a
19
+ * `Response` the caller consumes its own way. That is what lets one file
20
+ * serve a browser (BFF origin, cookie gates, service-worker download), a
21
+ * Slack bot (disk-backed streaming), an MCP server (in-memory) and an SDK
22
+ * (wildcard-CORS, no credentials) without any of them branching on who the
23
+ * others are.
24
+ *
25
+ * Two rules this file follows and the six copies did not:
26
+ *
27
+ * * Wire shapes are exported as they appear on the wire. Callers that
28
+ * want camelCase compose `toSealMetadata` / `toOwnerFiles`; callers that
29
+ * already speak snake_case (the web's views) keep doing so. One route,
30
+ * one method, one type.
31
+ * * Every failure is a `BackendError` carrying BOTH the coarse class the
32
+ * bots switch on (`not_found` / `revoked` / `rate_limited` / `auth` /
33
+ * `transient`) and the backend's own `{detail, code, retry_after}`
34
+ * envelope, which is what the web needs to tell `nda.required` from
35
+ * `otp.required` from a stale blob version.
36
+ */
37
+ /** Coarse failure class. Callers map this onto their own user-facing text. */
38
+ type BackendErrorCode = 'not_found' | 'revoked' | 'rate_limited' | 'auth' | 'transient' | 'unknown';
39
+ /**
40
+ * A non-2xx answer from the SEAL backend.
28
41
  *
29
- * Three stateful concerns the factory owns (all three skipped in
30
- * ephemeral mode, `state: null`, where nothing touches the disk):
31
- * - **Exclusive state lock (ISSUE-0623)**: `acquireStateLock` makes
32
- * this process the ONLY writer of the state dir for its lifetime;
33
- * a second `serve` on the same dir fails fast instead of silently
34
- * clobbering handles. Released in `close()` / on process exit.
35
- * - **State persistence**: each tool mutates `ctx.state` in-place
36
- * and calls `await ctx.persist()`; persist() re-encrypts the
37
- * view and writes atomically via `state.saveState`. A mutex
38
- * serialises persist() calls so concurrent tool calls cannot
39
- * race on the same on-disk envelope. Startup runs a journal
40
- * recovery pass (`recovery.ts`) before any tool registers.
41
- * - **Tool errors**: thrown `ToolError`s are converted to MCP's
42
- * `{ isError: true, content: [...] }` payload with the typed
43
- * `code` echoed back to the model. Other errors bubble as
44
- * internal_error so the model never sees raw stack traces.
42
+ * `code` is the coarse class (from the status). `appCode` is the backend's
43
+ * own machine-readable code from the `{detail, code, retry_after}` envelope
44
+ * — `nda.required`, `otp.required`, `blob.version_mismatch`, `seal.not_found`
45
+ * and friends. Callers that can act on the specific cause read `appCode`;
46
+ * callers that only need "gone vs denied vs try again" read `code`.
45
47
  */
46
-
47
- interface McpServerOptions {
48
- /** Encrypted state on disk; `null` = ephemeral: identity and handles in memory only. */
49
- readonly state: {
50
- readonly path: string;
51
- readonly passphrase: string;
52
- } | null;
53
- readonly backendBaseUrl?: string;
54
- readonly publicHost?: string;
55
- /** SEAL Pro workload; omitted = read the environment or the enrollment file, `null` = none. */
56
- readonly workload?: WorkloadCredentials | null;
48
+ declare class BackendError extends Error {
49
+ readonly code: BackendErrorCode;
50
+ readonly status: number;
51
+ /** Raw response body, when it was read. */
52
+ readonly body?: string;
53
+ /** `detail` from the canonical error envelope, when the body carried one. */
54
+ readonly detail?: string;
55
+ /** `code` from the canonical error envelope (e.g. `nda.required`). */
56
+ readonly appCode?: string;
57
+ /** `retry_after` seconds from the envelope, when present. */
58
+ readonly retryAfter?: number;
59
+ constructor(message: string, code: BackendErrorCode, status: number, extra?: {
60
+ body?: string;
61
+ detail?: string;
62
+ appCode?: string;
63
+ retryAfter?: number;
64
+ });
57
65
  }
58
- interface McpServerHandle {
59
- readonly mcp: McpServer;
60
- readonly transport: StdioServerTransport;
61
- readonly close: () => Promise<void>;
66
+ /** One file row of `GET /seal/{id}` (the public, recipient-facing view). */
67
+ interface SealFileWire {
68
+ id: string;
69
+ size: number;
70
+ sha256_hex: string | null;
71
+ name?: string;
72
+ mime?: string | null;
73
+ name_encrypted?: string;
74
+ name_nonce?: string;
75
+ mime_encrypted?: string;
76
+ mime_nonce?: string;
77
+ is_folder?: boolean;
78
+ parent_folder_id?: string | null;
79
+ revoked_at?: string | null;
62
80
  }
63
81
  /**
64
- * Bootstrap an MCP server. Loads + decrypts the state, instantiates
65
- * the backend client and the five tools, then connects over stdio.
66
- *
67
- * @throws `StatePassphraseInvalid` / `StateCorrupt` from state.ts.
68
- * @throws `Error` if `globalThis.fetch` is missing (Node <18).
82
+ * `GET /seal/{id}` — public metadata. No counter ticks here: `single_use`
83
+ * and `max_opens` are consumed by the ciphertext fetch, not by reading the
84
+ * description of a seal.
69
85
  */
70
- declare function createServer(options: McpServerOptions): Promise<McpServerHandle>;
71
-
86
+ interface SealMetaWire {
87
+ id: string;
88
+ expires_at: string;
89
+ revoked_at: string | null;
90
+ e2ee: boolean;
91
+ nda_required: boolean;
92
+ otp_required: boolean;
93
+ single_use: boolean;
94
+ max_opens?: number | null;
95
+ total_bytes?: number;
96
+ /**
97
+ * P1-1: the server's own verdict on the caller's gate session. Absent on
98
+ * pre-P1-1 backends, which is why the web keeps a cookie heuristic behind
99
+ * an explicit "legacy" branch rather than treating absence as "invalid".
100
+ */
101
+ nda_session_valid?: boolean;
102
+ otp_session_valid?: boolean;
103
+ files: SealFileWire[];
104
+ }
105
+ /** One file row of `GET /seal/{id}/owner` (includes revoked files). */
106
+ interface OwnerFileWire extends SealFileWire {
107
+ revoked_at?: string | null;
108
+ }
109
+ /** `GET /seal/{id}/owner` — the owner-authenticated view of a seal. */
110
+ interface OwnerSealWire {
111
+ id: string;
112
+ created_at: string;
113
+ expires_at: string;
114
+ revoked_at?: string | null;
115
+ e2ee: boolean;
116
+ nda_required: boolean;
117
+ otp_required?: boolean;
118
+ single_use: boolean;
119
+ geo_allowlist: string[] | null;
120
+ max_opens: number | null;
121
+ tier?: string;
122
+ total_bytes?: number;
123
+ files: OwnerFileWire[];
124
+ }
125
+ /** One row of the access log. IPs arrive truncated; the full value never leaves the DB. */
126
+ interface AuditEventWire {
127
+ id: string;
128
+ event: string;
129
+ ts: string;
130
+ country: string | null;
131
+ ip_truncated: string | null;
132
+ file_id: string | null;
133
+ filename: string | null;
134
+ range_request?: boolean;
135
+ }
136
+ /** Per-file aggregation of the access log. */
137
+ interface AuditSummaryWire {
138
+ file_id: string | null;
139
+ filename: string | null;
140
+ downloads: number;
141
+ countries: string[];
142
+ subnets: string[];
143
+ last_download_ts: string | null;
144
+ }
145
+ /** `GET /seal/{id}/audit` — the human-readable access log. */
146
+ interface AuditWire {
147
+ seal_id: string;
148
+ revoked_at: string | null;
149
+ events: AuditEventWire[];
150
+ summary: AuditSummaryWire[];
151
+ }
152
+ /** `POST /seal/{id}/subscriptions` — owner notification webhook. */
153
+ interface SubscriptionWire {
154
+ id: string;
155
+ secret: string;
156
+ }
157
+ /** What an intake request asks for (SPEC-AGENTS §4.4). */
158
+ type IntakeKind = 'secret' | 'file' | 'any';
72
159
  /**
73
- * Single URL resolver for the two origins the MCP server talks about
74
- * (ISSUE-0622).
75
- *
76
- * The two origins are INDEPENDENT in production:
77
- *
78
- * - backend → `https://api.seal.net` — FastAPI, where uploads /
79
- * metadata / revoke calls go;
80
- * - public → `https://seal.net` — the frontend that share
81
- * URLs must point at so a recipient's browser can open them.
82
- *
83
- * Before this module the public host was DERIVED from the backend URL
84
- * ("strip a trailing `/api`"), which worked for single-origin
85
- * self-hosted setups but produced `https://api.seal.net/s/…` links in
86
- * the default production configuration — the API origin serves no
87
- * frontend, so the link was dead on arrival.
88
- *
89
- * Resolution precedence (same for both origins):
90
- * 1. explicit value (CLI flag / `McpServerOptions` / library caller)
91
- * 2. environment variable (`SEAL_MCP_BACKEND_URL` / `SEAL_MCP_PUBLIC_HOST`)
92
- * 3. production default
160
+ * `/intake/{id}` — a person hands an agent a file or a secret through
161
+ * seal.net. `share_path` (`/s/{seal}/{code}#x25519:…`) appears once the page
162
+ * reported its seal, and only until the request expires; it opens only with
163
+ * the key named by `recipient_pub`.
164
+ */
165
+ interface IntakeWire {
166
+ id: string;
167
+ status: 'pending' | 'fulfilled' | 'expired';
168
+ label: string;
169
+ what: string;
170
+ where_url: string | null;
171
+ kind: IntakeKind;
172
+ recipient_pub: string;
173
+ expires_at: string;
174
+ share_path: string | null;
175
+ }
176
+ interface IntakeCreate {
177
+ /** The agent's X25519 public key, base64url without padding. */
178
+ readonly recipientPub: string;
179
+ readonly label: string;
180
+ readonly what: string;
181
+ readonly whereUrl?: string;
182
+ readonly kind?: IntakeKind;
183
+ readonly ttlSeconds?: number;
184
+ }
185
+ /** camelCase file row, for callers that prefer it (Slack, MCP). */
186
+ interface SealFileMeta {
187
+ readonly id: string;
188
+ readonly size: number;
189
+ readonly sha256_hex: string | null;
190
+ readonly name_encrypted?: string;
191
+ readonly name_nonce?: string;
192
+ readonly mime_encrypted?: string;
193
+ readonly mime_nonce?: string;
194
+ }
195
+ /** camelCase projection of `SealMetaWire`. */
196
+ interface SealMetadata {
197
+ readonly id: string;
198
+ readonly expiresAt: string;
199
+ readonly revokedAt: string | null;
200
+ readonly e2ee: boolean;
201
+ readonly ndaRequired: boolean;
202
+ readonly otpRequired: boolean;
203
+ readonly singleUse: boolean;
204
+ readonly maxOpens: number | null;
205
+ readonly files: SealFileMeta[];
206
+ }
207
+ interface SealApiOptions {
208
+ /**
209
+ * Origin (or origin + prefix) every path is appended to. The browser
210
+ * passes its BFF prefix (`/api`) so gate cookies land on the SPA origin;
211
+ * a bot passes the API origin directly.
212
+ */
213
+ readonly baseUrl: string;
214
+ /** Injected `fetch`. Defaults to the global one. */
215
+ readonly fetch?: typeof fetch;
216
+ /**
217
+ * Credentials mode for every request.
218
+ *
219
+ * The browser needs `'include'` for the OTP/NDA session cookies. The
220
+ * public send-only SDK needs `'omit'`: the wildcard-CORS send endpoints
221
+ * answer `Access-Control-Allow-Origin: *`, which browsers refuse for a
222
+ * credentialed request — send auth is the `X-Owner-Token` header, never
223
+ * a cookie. Node ignores the field.
224
+ */
225
+ readonly credentials?: RequestCredentials;
226
+ }
227
+ /** Per-call knobs shared by every method. */
228
+ interface RequestOptions {
229
+ readonly signal?: AbortSignal;
230
+ }
231
+ /** Knobs of `openDownload`. */
232
+ interface DownloadRequestOptions extends RequestOptions {
233
+ /**
234
+ * `false` hands `file.not_ready` to the caller at once. Default: ask again
235
+ * a few times (see `openDownload`).
236
+ */
237
+ readonly retryNotReady?: boolean;
238
+ }
239
+ /** Query parameters accepted by the download endpoint. */
240
+ interface DownloadTarget {
241
+ readonly fileId: string;
242
+ /** Public hand-out of a single file (SEAL Pro decision 2). */
243
+ readonly publicToken?: string;
244
+ /** Public hand-out of a whole seal. */
245
+ readonly viewToken?: string;
246
+ /**
247
+ * The caller is the seal's OWNER. A password, an NDA and a country list are
248
+ * what the owner asks of recipients, so a verified token passes them; it
249
+ * does not lift `single_use` / `max_opens`, which count every download
250
+ * (F-221). A token that does not verify is refused, not ignored.
251
+ */
252
+ readonly ownerToken?: string;
253
+ }
254
+ /** OTP verification input. `fileId`+`publicToken` scope the gate to one file. */
255
+ interface OtpVerification {
256
+ readonly code: string;
257
+ readonly fileId?: string;
258
+ readonly publicToken?: string;
259
+ }
260
+ /** NDA acceptance input, mirroring the backend's snake_case payload. */
261
+ interface NdaAcceptance {
262
+ readonly signature: string;
263
+ readonly timestamp: string;
264
+ readonly firstName: string;
265
+ readonly lastName: string;
266
+ readonly company?: string;
267
+ readonly fileId?: string;
268
+ readonly publicToken?: string;
269
+ /**
270
+ * The exact text the recipient saw. Transient — the backend renders it
271
+ * into the receipt PDF and never stores it.
272
+ */
273
+ readonly ndaText?: string;
274
+ }
275
+ /**
276
+ * The `/seal/*` client.
93
277
  *
94
- * Self-hosted deployments where frontend and backend live on other
95
- * origins must set BOTH knobs — the resolver never guesses one from
96
- * the other.
278
+ * Methods either return a parsed wire object or throw `BackendError`. Two
279
+ * of them (`openDownload`, `openAuditDownload`) return the raw `Response`
280
+ * instead: their bodies are bytes, and how bytes are consumed is a property
281
+ * of the runtime — the browser hands them to a service worker, the Slack
282
+ * bot streams them to disk, the MCP server buffers them.
97
283
  */
98
- declare const DEFAULT_BACKEND_URL = "https://api.seal.net";
99
- declare const DEFAULT_PUBLIC_HOST = "https://seal.net";
100
- declare const BACKEND_URL_ENV_VAR = "SEAL_MCP_BACKEND_URL";
101
- declare const PUBLIC_HOST_ENV_VAR = "SEAL_MCP_PUBLIC_HOST";
102
- /** Backend base URL: explicit → `SEAL_MCP_BACKEND_URL` → production default. */
103
- declare function resolveBackendUrl(explicit?: string): string;
104
- /** Public share-URL origin: explicit → `SEAL_MCP_PUBLIC_HOST` → production default. */
105
- declare function resolvePublicHost(explicit?: string): string;
284
+ declare class SealApi {
285
+ readonly baseUrl: string;
286
+ private readonly injectedFetch;
287
+ private readonly credentials;
288
+ constructor(options: SealApiOptions);
289
+ /**
290
+ * The transport for one request.
291
+ *
292
+ * Called with `globalThis` as receiver, ALWAYS. Storing `globalThis.fetch`
293
+ * on an instance and calling it as `this.fetchImpl(...)` hands the browser a
294
+ * receiver that is not `window`, and it answers "Failed to execute 'fetch'
295
+ * on 'Window': Illegal invocation". Node does not care, which is exactly why
296
+ * the previous copies of this client — all of them Node-only — could carry
297
+ * the same line for a year without anyone noticing.
298
+ *
299
+ * Resolved per call rather than captured in the constructor: a browser host
300
+ * may replace `window.fetch` after this module is evaluated (SEAL Pro does,
301
+ * to re-anchor root-absolute paths onto its `/pro` base path), and clients
302
+ * built as module-level singletons are constructed before that happens.
303
+ */
304
+ private fetchImpl;
305
+ /** Absolute (or origin-relative) URL for a path that must start with `/`. */
306
+ url(path: string): string;
307
+ private request;
308
+ private static ownerHeaders;
309
+ /** `GET /seal/{id}` — public metadata. Ticks no counters. */
310
+ getMetadata(sealId: string, opts?: RequestOptions): Promise<SealMetaWire>;
311
+ /**
312
+ * `GET /seal/{id}/share/{code}` — the recipient's encrypted key blob.
313
+ *
314
+ * Public by construction: the blob is opaque ciphertext, so the server has
315
+ * nothing to gate on. Reading it ticks no counter, which is what lets a
316
+ * client show the file list before the recipient commits to a download.
317
+ */
318
+ getShareBlob(sealId: string, shareCode: string, opts?: RequestOptions): Promise<Uint8Array<ArrayBuffer>>;
319
+ /**
320
+ * `POST /seal/{id}/otp` — verify the recipient password.
321
+ *
322
+ * The backend answers with `Set-Cookie: otp_{sealId}=…`. A browser stores
323
+ * it and cannot read it back (`sessionCookie` is null there, by design);
324
+ * Node has no cookie jar, so the pair is extracted and returned for the
325
+ * caller to forward on the download.
326
+ */
327
+ verifyOtp(sealId: string, input: OtpVerification, opts?: RequestOptions): Promise<{
328
+ status: string;
329
+ sessionCookie: string | null;
330
+ }>;
331
+ /** `POST /seal/{id}/nda` — record the recipient's signature. */
332
+ acceptNda(sealId: string, input: NdaAcceptance, opts?: RequestOptions): Promise<{
333
+ status: string;
334
+ }>;
335
+ /** The ciphertext-download URL. Building it is separate from fetching it. */
336
+ downloadUrl(sealId: string, target: DownloadTarget): string;
337
+ /**
338
+ * `GET /seal/{id}/download` — the ciphertext, as a live `Response`.
339
+ *
340
+ * THIS is the call that consumes `single_use` / `max_opens`, so every
341
+ * policy check a caller wants to make belongs before it: once a counter
342
+ * ticks it stays ticked, whatever the caller does with the bytes.
343
+ *
344
+ * The backend answers 307 to a signed storage URL for EVERY seal — it never
345
+ * serves a byte itself (audit/SPEC-UPLOAD-SCALE.md §6). A runtime whose
346
+ * `fetch` follows redirects (Node: Slack, MCP, SDK) gets the ciphertext
347
+ * from storage here; the browser talks to its BFF, which hands the same
348
+ * URL back as JSON because script cannot read `Location`. The body is left
349
+ * unread on purpose: how bytes are consumed is the runtime's business.
350
+ *
351
+ * `file.not_ready` (409, "clients should retry after retry_after") is
352
+ * asked again, up to NOT_READY_MAX_RETRIES times, pausing `retry_after`
353
+ * seconds. It is a SAFETY NET, not the mechanism: the sender waits for
354
+ * verification before handing out a link, and recipients are only listed
355
+ * verified files, so this is met by a client that kept an older listing or
356
+ * by a verification slower than the sender's wait. Few and spaced, on
357
+ * purpose — every refused attempt writes a line to the owner's access log
358
+ * and spends the download rate limit (F-162). Asking again is safe: the
359
+ * backend refuses an unverified file BEFORE it counts an open
360
+ * (`seal/download.py`, pinned by `test_download_gate_order.py`).
361
+ *
362
+ * Its terminal twin, `file.verification_failed`, is never retried — the
363
+ * bytes were rejected and are being burned. It is recognisable only by
364
+ * `appCode`: its HTTP status is 410, which `code` folds into `revoked`.
365
+ */
366
+ openDownload(sealId: string, target: DownloadTarget & {
367
+ readonly sessionCookie?: string;
368
+ }, opts?: DownloadRequestOptions): Promise<Response>;
369
+ /** `GET /seal/{id}/owner` — the owner's view, revoked files included. */
370
+ getOwnerSeal(sealId: string, ownerToken: string, opts?: RequestOptions): Promise<OwnerSealWire>;
371
+ /**
372
+ * `GET /seal/{id}/blob` — the owner's encrypted key blob.
373
+ *
374
+ * Public endpoint (opaque ciphertext). The `ETag` travels back because a
375
+ * writer must send it as `If-Match`: the blob is read-modify-written by
376
+ * every append, and two tabs doing that without CAS silently lose keys.
377
+ */
378
+ readOwnerBlob(sealId: string, opts?: RequestOptions): Promise<{
379
+ blob: Uint8Array<ArrayBuffer>;
380
+ etag: string | null;
381
+ }>;
382
+ /**
383
+ * `PATCH /seal/{id}/blob` — replace the owner's encrypted key blob.
384
+ *
385
+ * `ifMatch` opts into the CAS contract; without it the backend takes the
386
+ * legacy last-write-wins path. A stale version comes back as a
387
+ * `BackendError` with `status === 412` and `appCode === 'blob.version_mismatch'`.
388
+ */
389
+ writeOwnerBlob(sealId: string, ownerToken: string, encryptedBlobB64: string, opts?: RequestOptions & {
390
+ readonly ifMatch?: string | null;
391
+ }): Promise<{
392
+ etag: string | null;
393
+ }>;
394
+ /** `POST /seal/{id}/share` — mint a share link, returns its code. */
395
+ createShareLink(sealId: string, ownerToken: string, encryptedBlobB64: string, opts?: RequestOptions): Promise<string>;
396
+ /**
397
+ * `POST /seal/{id}/share/{code}/revoke` — revoke one share link.
398
+ * Idempotent: an already-revoked link is success, not a failure.
399
+ */
400
+ revokeShareLink(sealId: string, shareCode: string, ownerToken: string, opts?: RequestOptions): Promise<void>;
401
+ /**
402
+ * `POST /seal/{id}/revoke` — revoke the whole seal.
403
+ *
404
+ * Idempotent, including at the extremes: a seal that is already gone (404)
405
+ * or already revoked (410) satisfies the caller's intent, so both count as
406
+ * success. `revokedAt` is null when the server had nothing left to report.
407
+ */
408
+ revoke(sealId: string, ownerToken: string, opts?: RequestOptions): Promise<{
409
+ status: string;
410
+ revokedAt: string | null;
411
+ }>;
412
+ /**
413
+ * `POST /seal/{id}/files/{fileId}/revoke` — revoke a single file.
414
+ * Idempotent for the same reason as the share-link revoke.
415
+ */
416
+ revokeFile(sealId: string, fileId: string, ownerToken: string, opts?: RequestOptions): Promise<void>;
417
+ /** `GET /seal/{id}/audit` — the access log, PII already truncated server-side. */
418
+ getAudit(sealId: string, ownerToken: string, opts?: RequestOptions): Promise<AuditWire>;
419
+ /**
420
+ * `GET /seal/{id}/audit/download` — the signed JSONL export, as a live
421
+ * `Response`. It is an evidentiary artifact (Ed25519 over each line), so
422
+ * the bytes are handed over untouched.
423
+ */
424
+ openAuditDownload(sealId: string, ownerToken: string, opts?: RequestOptions): Promise<Response>;
425
+ /**
426
+ * `POST /seal/{id}/subscriptions` — register an owner-notification
427
+ * webhook. The derived HMAC secret is returned exactly once.
428
+ */
429
+ createSubscription(sealId: string, ownerToken: string, body?: {
430
+ readonly reference?: string;
431
+ }, opts?: RequestOptions): Promise<SubscriptionWire>;
432
+ /**
433
+ * `POST /billing/checkout` — the Stripe payment page for a seal registered
434
+ * above the free limit (SPEC-AGENTS §5.2). A repeat call within the
435
+ * session's life returns the same page.
436
+ */
437
+ createCheckout(sealId: string, ownerToken: string, tier: string, opts?: RequestOptions): Promise<string>;
438
+ /** `POST /intake` — open a request; the person answers at `/i/{id}`. */
439
+ createIntake(input: IntakeCreate, opts?: RequestOptions): Promise<IntakeWire>;
440
+ /**
441
+ * `GET /intake/{id}` — the request as it stands. `waitSeconds` (at most
442
+ * `limits.json::INTAKE_WAIT_MAX_SECONDS`) holds the answer until the page
443
+ * reports or the wait runs out.
444
+ */
445
+ getIntake(intakeId: string, opts?: RequestOptions & {
446
+ readonly waitSeconds?: number;
447
+ }): Promise<IntakeWire>;
448
+ /** `POST /intake/{id}/fulfil` — the page reports the seal it made; once. */
449
+ fulfilIntake(intakeId: string, link: {
450
+ readonly sealId: string;
451
+ readonly shareCode: string;
452
+ readonly fragment: string;
453
+ }, opts?: RequestOptions): Promise<IntakeWire>;
454
+ }
106
455
 
107
456
  /**
108
457
  * Streaming Uploader Types
@@ -419,6 +768,115 @@ interface SingleFileUploadResult {
419
768
  */
420
769
  declare function uploadSingleFile(ctx: UploadContext, file: File, policy?: UploadPolicy, callbacks?: UploadCallbacks): Promise<SingleFileUploadResult>;
421
770
 
771
+ /** What a workload needs to be itself: which workload, which API, and its private seed. */
772
+ interface WorkloadCredentials {
773
+ workloadId: string;
774
+ /** API base URL including `/api/v1`; in production `https://api.seal.net/pro-api/api/v1`. */
775
+ baseUrl: string;
776
+ /** The 64-byte seed (base64) both private keys derive from. */
777
+ seedB64: string;
778
+ }
779
+
780
+ /**
781
+ * MCP server bootstrap — wires the five tools (`seal_share`,
782
+ * `seal_request`, `seal_open`, `seal_list`, `seal_revoke`) to a
783
+ * stdio-transport `McpServer` instance, and `seal_pro_secret_use`,
784
+ * `seal_pro_secret_request` and `seal_pro_file_get` when SEAL Pro workload
785
+ * credentials exist (contract `pro_tools`).
786
+ *
787
+ * The bootstrap is intentionally factored as a `createServer()`
788
+ * factory that returns an awaitable handle, NOT a hard-coded
789
+ * `main()` entry. That shape:
790
+ * - lets `cli.ts` configure passphrase delivery, state path,
791
+ * backend base URL, etc. before launching;
792
+ * - lets test code spawn an in-process MCP without spawning a
793
+ * subprocess;
794
+ * - keeps the "where do creds come from?" decision out of the
795
+ * pure tool implementations.
796
+ *
797
+ * Tool `title`, `description` and `annotations` come from
798
+ * `shared/contracts/seal_mcp.v1.json` (contract first, §6.2), and so do
799
+ * the descriptions of the inputs the contract describes; the zod input
800
+ * schemas live here.
801
+ *
802
+ * Three stateful concerns the factory owns (all three skipped in
803
+ * ephemeral mode, `state: null`, where nothing touches the disk):
804
+ * - **Exclusive state lock (ISSUE-0623)**: `acquireStateLock` makes
805
+ * this process the ONLY writer of the state dir for its lifetime;
806
+ * a second `serve` on the same dir fails fast instead of silently
807
+ * clobbering handles. Released in `close()` / on process exit.
808
+ * - **State persistence**: each tool mutates `ctx.state` in-place
809
+ * and calls `await ctx.persist()`; persist() re-encrypts the
810
+ * view and writes atomically via `state.saveState`. A mutex
811
+ * serialises persist() calls so concurrent tool calls cannot
812
+ * race on the same on-disk envelope. Startup runs a journal
813
+ * recovery pass (`recovery.ts`) before any tool registers.
814
+ * - **Tool errors**: thrown `ToolError`s are converted to MCP's
815
+ * `{ isError: true, content: [...] }` payload with the typed
816
+ * `code` echoed back to the model. Other errors bubble as
817
+ * internal_error so the model never sees raw stack traces.
818
+ */
819
+
820
+ interface McpServerOptions {
821
+ /** Encrypted state on disk; `null` = ephemeral: identity and handles in memory only. */
822
+ readonly state: {
823
+ readonly path: string;
824
+ readonly passphrase: string;
825
+ } | null;
826
+ readonly backendBaseUrl?: string;
827
+ readonly publicHost?: string;
828
+ /** SEAL Pro workload; omitted = read the environment or the enrollment file, `null` = none. */
829
+ readonly workload?: WorkloadCredentials | null;
830
+ }
831
+ interface McpServerHandle {
832
+ readonly mcp: McpServer;
833
+ readonly transport: StdioServerTransport;
834
+ readonly close: () => Promise<void>;
835
+ }
836
+ /**
837
+ * Bootstrap an MCP server. Loads + decrypts the state, instantiates
838
+ * the backend client and the five tools, then connects over stdio.
839
+ *
840
+ * @throws `StatePassphraseInvalid` / `StateCorrupt` from state.ts.
841
+ * @throws `Error` if `globalThis.fetch` is missing (Node <18).
842
+ */
843
+ declare function createServer(options: McpServerOptions): Promise<McpServerHandle>;
844
+
845
+ /**
846
+ * Single URL resolver for the two origins the MCP server talks about
847
+ * (ISSUE-0622).
848
+ *
849
+ * The two origins are INDEPENDENT in production:
850
+ *
851
+ * - backend → `https://api.seal.net` — FastAPI, where uploads /
852
+ * metadata / revoke calls go;
853
+ * - public → `https://seal.net` — the frontend that share
854
+ * URLs must point at so a recipient's browser can open them.
855
+ *
856
+ * Before this module the public host was DERIVED from the backend URL
857
+ * ("strip a trailing `/api`"), which worked for single-origin
858
+ * self-hosted setups but produced `https://api.seal.net/s/…` links in
859
+ * the default production configuration — the API origin serves no
860
+ * frontend, so the link was dead on arrival.
861
+ *
862
+ * Resolution precedence (same for both origins):
863
+ * 1. explicit value (CLI flag / `McpServerOptions` / library caller)
864
+ * 2. environment variable (`SEAL_MCP_BACKEND_URL` / `SEAL_MCP_PUBLIC_HOST`)
865
+ * 3. production default
866
+ *
867
+ * Self-hosted deployments where frontend and backend live on other
868
+ * origins must set BOTH knobs — the resolver never guesses one from
869
+ * the other.
870
+ */
871
+ declare const DEFAULT_BACKEND_URL = "https://api.seal.net";
872
+ declare const DEFAULT_PUBLIC_HOST = "https://seal.net";
873
+ declare const BACKEND_URL_ENV_VAR = "SEAL_MCP_BACKEND_URL";
874
+ declare const PUBLIC_HOST_ENV_VAR = "SEAL_MCP_PUBLIC_HOST";
875
+ /** Backend base URL: explicit → `SEAL_MCP_BACKEND_URL` → production default. */
876
+ declare function resolveBackendUrl(explicit?: string): string;
877
+ /** Public share-URL origin: explicit → `SEAL_MCP_PUBLIC_HOST` → production default. */
878
+ declare function resolvePublicHost(explicit?: string): string;
879
+
422
880
  /**
423
881
  * Out-of-band delivery of the share link to the human user. The link is
424
882
  * the whole capability (key in the fragment); there is no password.
@@ -963,7 +1421,7 @@ declare function promptHiddenPassphrase(prompt: string): Promise<string>;
963
1421
  interface ToolContext {
964
1422
  readonly state: StateView;
965
1423
  readonly identity: Identity;
966
- readonly client: S;
1424
+ readonly client: SealApi;
967
1425
  /**
968
1426
  * Persistence hook. The server is responsible for serialising and
969
1427
  * encrypting `ctx.state` to disk after a successful mutation; tools
@@ -1320,4 +1778,4 @@ interface SealShareDeps {
1320
1778
  }
1321
1779
  declare function sealShare(ctx: ToolContext, input: SealShareInput, deps?: SealShareDeps): Promise<SealShareOutput>;
1322
1780
 
1323
- export { APP_NAME, type AcquireLockDeps, type AuditEntry, BACKEND_URL_ENV_VAR, DEFAULT_BACKEND_URL, DEFAULT_PUBLIC_HOST, type HandleEntry, HandoffBothChannelsFailedError, type HandoffPayload, type HandoffResult, type Identity, type IdentityFields, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE, type KeytarLike, KeytarUnavailableError, LOCK_FILE_NAME, type LockHolderInfo, type McpServerHandle, type McpServerOptions, PASSPHRASE_ENV_VAR, PUBLIC_HOST_ENV_VAR, PassphraseNotFoundError, type PassphraseSource, type PathResolution, type PendingCompensation, type PendingOperation, type PendingStep, type RecoveryReport, type ResolveOptions, type ResolvedPassphrase, type RevokeClient, STATE_FILE_NAME, type SealListInput, type SealListOutput, type SealOpenInput, type SealOpenOutput, type SealRevokeInput, type SealRevokeOutput, type SealShareInput, type SealShareMode, type SealShareOutput, StateCorrupt, type StateDirSource, type StateLock, StateLockedError, StatePassphraseInvalid, type StateView, ToolError, acquireStateLock, createEmptyState, createServer, deleteKeychainPassphrase, generateHandle, generateIdentity, identityToFields, isValidHandle, loadIdentity, loadState, performHandoff, probeKeychain, probeStateLock, promptHiddenPassphrase, recoverPendingOperations, resolveBackendUrl, resolvePassphraseFromStores, resolvePublicHost, resolveStateDir, saveState, sealList, sealOpen, sealRevoke, sealShare, setKeychainPassphrase };
1781
+ export { APP_NAME, type AcquireLockDeps, type AuditEntry, BACKEND_URL_ENV_VAR, BackendError, DEFAULT_BACKEND_URL, DEFAULT_PUBLIC_HOST, type HandleEntry, HandoffBothChannelsFailedError, type HandoffPayload, type HandoffResult, type Identity, type IdentityFields, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE, type KeytarLike, KeytarUnavailableError, LOCK_FILE_NAME, type LockHolderInfo, type McpServerHandle, type McpServerOptions, PASSPHRASE_ENV_VAR, PUBLIC_HOST_ENV_VAR, PassphraseNotFoundError, type PassphraseSource, type PathResolution, type PendingCompensation, type PendingOperation, type PendingStep, type RecoveryReport, type ResolveOptions, type ResolvedPassphrase, type RevokeClient, STATE_FILE_NAME, SealApi, type SealApiOptions, type SealListInput, type SealListOutput, type SealMetadata, type SealOpenInput, type SealOpenOutput, type SealRevokeInput, type SealRevokeOutput, type SealShareInput, type SealShareMode, type SealShareOutput, StateCorrupt, type StateDirSource, type StateLock, StateLockedError, StatePassphraseInvalid, type StateView, ToolError, acquireStateLock, createEmptyState, createServer, deleteKeychainPassphrase, generateHandle, generateIdentity, identityToFields, isValidHandle, loadIdentity, loadState, performHandoff, probeKeychain, probeStateLock, promptHiddenPassphrase, recoverPendingOperations, resolveBackendUrl, resolvePassphraseFromStores, resolvePublicHost, resolveStateDir, saveState, sealList, sealOpen, sealRevoke, sealShare, setKeychainPassphrase };