sealnet-mcp 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -17
- package/dist/cli.cjs +980 -997
- package/dist/cli.d.cts +10 -7
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +3719 -3736
- package/dist/index.cjs +357 -276
- package/dist/index.d.cts +662 -178
- package/dist/index.d.ts +662 -178
- package/dist/index.js +335 -262
- package/package.json +2 -2
package/dist/index.d.ts
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
readonly
|
|
55
|
-
/**
|
|
56
|
-
readonly
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
*
|
|
65
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
74
|
-
* (
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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,110 @@ 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` open the state with the agent's key, resolve the
|
|
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
|
+
* Two stateful concerns the factory owns:
|
|
803
|
+
* - **State (SPEC-ADDRESS F1 §4)**: every MCP host on the machine runs
|
|
804
|
+
* its own `serve` on the same state, so each tool change goes through
|
|
805
|
+
* `ctx.update` = `state.updateState`: under the state lock, read the
|
|
806
|
+
* file anew, apply the change, save. Startup runs a journal recovery
|
|
807
|
+
* pass (`recovery.ts`) the same way, before any tool registers. In
|
|
808
|
+
* ephemeral mode (`state: null`) the change applies to memory only.
|
|
809
|
+
* - **Tool errors**: thrown `ToolError`s (and `state_busy`) are
|
|
810
|
+
* converted to MCP's `{ isError: true, content: [...] }` payload with
|
|
811
|
+
* the typed `code` echoed back to the model. Other errors bubble as
|
|
812
|
+
* internal_error so the model never sees raw stack traces.
|
|
813
|
+
*/
|
|
814
|
+
|
|
815
|
+
interface McpServerOptions {
|
|
816
|
+
/** Encrypted state on disk (`seed.ts::openAgentState`); `null` = ephemeral: a new identity, handles in memory only. */
|
|
817
|
+
readonly state: {
|
|
818
|
+
readonly path: string;
|
|
819
|
+
readonly passphrase: string;
|
|
820
|
+
} | null;
|
|
821
|
+
readonly backendBaseUrl?: string;
|
|
822
|
+
readonly publicHost?: string;
|
|
823
|
+
/** SEAL Pro workload; omitted = read the environment or the enrollment file, `null` = none. */
|
|
824
|
+
readonly workload?: WorkloadCredentials | null;
|
|
825
|
+
}
|
|
826
|
+
interface McpServerHandle {
|
|
827
|
+
readonly mcp: McpServer;
|
|
828
|
+
readonly transport: StdioServerTransport;
|
|
829
|
+
readonly close: () => Promise<void>;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Bootstrap an MCP server. Loads + decrypts the state, instantiates
|
|
833
|
+
* the backend client and the five tools, then connects over stdio.
|
|
834
|
+
*
|
|
835
|
+
* @throws `StatePassphraseInvalid` / `StateCorrupt` from state.ts.
|
|
836
|
+
* @throws `Error` if `globalThis.fetch` is missing (Node <18).
|
|
837
|
+
*/
|
|
838
|
+
declare function createServer(options: McpServerOptions): Promise<McpServerHandle>;
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Single URL resolver for the two origins the MCP server talks about
|
|
842
|
+
* (ISSUE-0622).
|
|
843
|
+
*
|
|
844
|
+
* The two origins are INDEPENDENT in production:
|
|
845
|
+
*
|
|
846
|
+
* - backend → `https://api.seal.net` — FastAPI, where uploads /
|
|
847
|
+
* metadata / revoke calls go;
|
|
848
|
+
* - public → `https://seal.net` — the frontend that share
|
|
849
|
+
* URLs must point at so a recipient's browser can open them.
|
|
850
|
+
*
|
|
851
|
+
* Before this module the public host was DERIVED from the backend URL
|
|
852
|
+
* ("strip a trailing `/api`"), which worked for single-origin
|
|
853
|
+
* self-hosted setups but produced `https://api.seal.net/s/…` links in
|
|
854
|
+
* the default production configuration — the API origin serves no
|
|
855
|
+
* frontend, so the link was dead on arrival.
|
|
856
|
+
*
|
|
857
|
+
* Resolution precedence (same for both origins):
|
|
858
|
+
* 1. explicit value (CLI flag / `McpServerOptions` / library caller)
|
|
859
|
+
* 2. environment variable (`SEAL_MCP_BACKEND_URL` / `SEAL_MCP_PUBLIC_HOST`)
|
|
860
|
+
* 3. production default
|
|
861
|
+
*
|
|
862
|
+
* Self-hosted deployments where frontend and backend live on other
|
|
863
|
+
* origins must set BOTH knobs — the resolver never guesses one from
|
|
864
|
+
* the other.
|
|
865
|
+
*/
|
|
866
|
+
declare const DEFAULT_BACKEND_URL = "https://api.seal.net";
|
|
867
|
+
declare const DEFAULT_PUBLIC_HOST = "https://seal.net";
|
|
868
|
+
declare const BACKEND_URL_ENV_VAR = "SEAL_MCP_BACKEND_URL";
|
|
869
|
+
declare const PUBLIC_HOST_ENV_VAR = "SEAL_MCP_PUBLIC_HOST";
|
|
870
|
+
/** Backend base URL: explicit → `SEAL_MCP_BACKEND_URL` → production default. */
|
|
871
|
+
declare function resolveBackendUrl(explicit?: string): string;
|
|
872
|
+
/** Public share-URL origin: explicit → `SEAL_MCP_PUBLIC_HOST` → production default. */
|
|
873
|
+
declare function resolvePublicHost(explicit?: string): string;
|
|
874
|
+
|
|
422
875
|
/**
|
|
423
876
|
* Out-of-band delivery of the share link to the human user. The link is
|
|
424
877
|
* the whole capability (key in the fragment); there is no password.
|
|
@@ -595,6 +1048,15 @@ interface PendingOperation {
|
|
|
595
1048
|
* backend — see `HandleEntry.backend_url`.
|
|
596
1049
|
*/
|
|
597
1050
|
backend_url?: string;
|
|
1051
|
+
/**
|
|
1052
|
+
* The process running the operation (SPEC-ADDRESS F1 §4): several MCP
|
|
1053
|
+
* hosts share this state, and recovery leaves an entry alone while its
|
|
1054
|
+
* owner may still be running. Absent on 0.2.x entries.
|
|
1055
|
+
*/
|
|
1056
|
+
readonly owner?: {
|
|
1057
|
+
readonly pid: number;
|
|
1058
|
+
readonly hostname: string;
|
|
1059
|
+
};
|
|
598
1060
|
}
|
|
599
1061
|
interface AuditEntry {
|
|
600
1062
|
readonly ts: string;
|
|
@@ -649,6 +1111,19 @@ declare function loadState(passphrase: string, statePath: string): Promise<State
|
|
|
649
1111
|
* is non-adaptive — but cheap and follows defence-in-depth).
|
|
650
1112
|
*/
|
|
651
1113
|
declare function saveState(state: StateView, passphrase: string, statePath: string): Promise<void>;
|
|
1114
|
+
/**
|
|
1115
|
+
* A change to the state: insertions, and deletions as explicit operations
|
|
1116
|
+
* (a key missing from one process's view must not delete what another
|
|
1117
|
+
* process wrote). `false` = nothing to write, only read the file anew.
|
|
1118
|
+
*/
|
|
1119
|
+
type StateMutation = (state: StateView) => boolean | void | Promise<boolean | void>;
|
|
1120
|
+
/**
|
|
1121
|
+
* The one way to change the state on disk (SPEC-ADDRESS F1 §4): under the
|
|
1122
|
+
* state lock, read `state.json` anew, apply `mutate`, save. Several MCP hosts
|
|
1123
|
+
* share the file, so a process never writes its own copy of the whole view.
|
|
1124
|
+
* Returns the state as it now is.
|
|
1125
|
+
*/
|
|
1126
|
+
declare function updateState(passphrase: string, statePath: string, mutate: StateMutation): Promise<StateView>;
|
|
652
1127
|
|
|
653
1128
|
/**
|
|
654
1129
|
* X25519 identity for the MCP server (incoming targeted-share recipient).
|
|
@@ -658,9 +1133,10 @@ declare function saveState(state: StateView, passphrase: string, statePath: stri
|
|
|
658
1133
|
* `seal keygen`, the SDK and `sealnet` use — byte-for-byte compatible by
|
|
659
1134
|
* construction, not by spec.
|
|
660
1135
|
*
|
|
661
|
-
* The
|
|
662
|
-
*
|
|
663
|
-
*
|
|
1136
|
+
* The secret is the agent's root secret (`seed.ts`, SPEC-ADDRESS F1); the
|
|
1137
|
+
* `Identity` view is reconstructed from `state.identity`, which must equal
|
|
1138
|
+
* it, on every server start. The serialised form lives inside the
|
|
1139
|
+
* AES-256-GCM ciphertext of `state.ts`.
|
|
664
1140
|
*/
|
|
665
1141
|
|
|
666
1142
|
interface Identity {
|
|
@@ -669,6 +1145,8 @@ interface Identity {
|
|
|
669
1145
|
}
|
|
670
1146
|
/** A fresh X25519 keypair for this server. */
|
|
671
1147
|
declare function generateIdentity(): Promise<Identity>;
|
|
1148
|
+
/** The keypair of a root secret (`seed.ts`): the seed is the X25519 secret. */
|
|
1149
|
+
declare function identityFromSeed(seed: Uint8Array): Promise<Identity>;
|
|
672
1150
|
/**
|
|
673
1151
|
* Project an `Identity` into the serialised form that lives in
|
|
674
1152
|
* `state.identity` (base64-encoded raw bytes; matches contract
|
|
@@ -683,22 +1161,22 @@ declare function identityToFields(id: Identity): IdentityFields;
|
|
|
683
1161
|
declare function loadIdentity(fields: IdentityFields): Promise<Identity>;
|
|
684
1162
|
|
|
685
1163
|
/**
|
|
686
|
-
*
|
|
1164
|
+
* The seal-mcp state lock, taken for each write (ISSUE-0623, SPEC-ADDRESS F1 §4).
|
|
687
1165
|
*
|
|
688
1166
|
* # Why
|
|
689
1167
|
*
|
|
690
|
-
* `state.json` is a whole-envelope read-modify-write store
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
1168
|
+
* `state.json` is a whole-envelope read-modify-write store. Every MCP host on
|
|
1169
|
+
* the machine (Claude Desktop, Claude Code, Cursor) spawns its own
|
|
1170
|
+
* `sealnet-mcp serve` on the same state dir, and all of them must work: one
|
|
1171
|
+
* seed, one address. So a process does not own the state for its lifetime;
|
|
1172
|
+
* each change re-reads the file, applies this process's change and saves
|
|
1173
|
+
* (`state.ts::updateState`), and the lock makes that sequence exclusive. Two
|
|
1174
|
+
* writers without it would overwrite each other's handles: last writer wins,
|
|
1175
|
+
* the other process's owner tokens are gone forever.
|
|
697
1176
|
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* corrupting capability state.
|
|
1177
|
+
* Waiting for the lock ends after `LOCK_WAIT_MS` with `StateBusyError`
|
|
1178
|
+
* (`state_busy`). Inside one process the writes queue (`withStateLock`): the
|
|
1179
|
+
* lock file names a pid, and the process would otherwise wait for itself.
|
|
702
1180
|
*
|
|
703
1181
|
* # Mechanism (DR-mcp: TOCTOU-hardened)
|
|
704
1182
|
*
|
|
@@ -719,10 +1197,11 @@ declare function loadIdentity(fields: IdentityFields): Promise<Identity>;
|
|
|
719
1197
|
* fresh holder slipped in between probe and rename), and only then
|
|
720
1198
|
* creates its own lock. A lock from a different hostname (network
|
|
721
1199
|
* volume) is never broken automatically — we cannot probe liveness
|
|
722
|
-
* across hosts.
|
|
1200
|
+
* across hosts. The same rule (`holderMayRun`) tells journal recovery
|
|
1201
|
+
* whose upload it must leave alone.
|
|
723
1202
|
*
|
|
724
1203
|
* PID-recycling is the accepted residual risk: the probe window is
|
|
725
|
-
* milliseconds and the failure mode (
|
|
1204
|
+
* milliseconds and the failure mode (waiting, then `state_busy`) is safe.
|
|
726
1205
|
*
|
|
727
1206
|
* Release verifies ownership: the lock is unlinked only if it still
|
|
728
1207
|
* carries OUR pid+hostname, so a process can never release a lock a
|
|
@@ -741,6 +1220,11 @@ declare class StateLockedError extends Error {
|
|
|
741
1220
|
readonly lockPath: string;
|
|
742
1221
|
constructor(lockPath: string, holder: LockHolderInfo | null);
|
|
743
1222
|
}
|
|
1223
|
+
/** The lock stayed taken for `LOCK_WAIT_MS`; nothing was written (contract `state_busy`). */
|
|
1224
|
+
declare class StateBusyError extends Error {
|
|
1225
|
+
readonly code = "state_busy";
|
|
1226
|
+
constructor(locked: StateLockedError);
|
|
1227
|
+
}
|
|
744
1228
|
interface StateLock {
|
|
745
1229
|
readonly lockPath: string;
|
|
746
1230
|
/** Idempotent. Removes the lock file (if still ours) and the process-exit hook. */
|
|
@@ -754,6 +1238,11 @@ interface AcquireLockDeps {
|
|
|
754
1238
|
/** Override hostname for tests. */
|
|
755
1239
|
readonly host?: string;
|
|
756
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Whether the process a lock or a journal entry names may still be running:
|
|
1243
|
+
* alive on this host, or on another host, which cannot be probed.
|
|
1244
|
+
*/
|
|
1245
|
+
declare function holderMayRun(holder: Pick<LockHolderInfo, 'pid' | 'hostname'>, deps?: AcquireLockDeps): boolean;
|
|
757
1246
|
/**
|
|
758
1247
|
* Acquire the exclusive state-dir lock or throw `StateLockedError`.
|
|
759
1248
|
*
|
|
@@ -763,6 +1252,11 @@ interface AcquireLockDeps {
|
|
|
763
1252
|
* `mode` does not repair an existing directory).
|
|
764
1253
|
*/
|
|
765
1254
|
declare function acquireStateLock(stateDir: string, deps?: AcquireLockDeps): Promise<StateLock>;
|
|
1255
|
+
/**
|
|
1256
|
+
* Run `fn` holding the state lock: waits up to `LOCK_WAIT_MS` for another
|
|
1257
|
+
* process to finish its write, then fails with `StateBusyError`.
|
|
1258
|
+
*/
|
|
1259
|
+
declare function withStateLock<T>(stateDir: string, fn: () => Promise<T>, deps?: AcquireLockDeps): Promise<T>;
|
|
766
1260
|
/**
|
|
767
1261
|
* Read-only probe for `doctor`: report who (if anyone) holds the lock
|
|
768
1262
|
* and whether it looks stale from this host's perspective.
|
|
@@ -776,8 +1270,10 @@ declare function probeStateLock(stateDir: string, deps?: AcquireLockDeps): Promi
|
|
|
776
1270
|
/**
|
|
777
1271
|
* Startup recovery for the durable operation journal (ISSUE-0623).
|
|
778
1272
|
*
|
|
779
|
-
* Runs once in `createServer()
|
|
780
|
-
*
|
|
1273
|
+
* Runs once in `createServer()`, under the state lock (`updateState`),
|
|
1274
|
+
* BEFORE any tool can execute. Several MCP hosts share the state
|
|
1275
|
+
* (SPEC-ADDRESS F1 §4), so an entry whose `owner` may still be running
|
|
1276
|
+
* is a neighbour's upload in progress and is left alone. Every other
|
|
781
1277
|
* `state.pending` entry is a `seal_share` that crashed (or lost its
|
|
782
1278
|
* process) mid-flight; the step recorded at the last checkpoint tells
|
|
783
1279
|
* us exactly what is safe to do:
|
|
@@ -827,7 +1323,7 @@ interface RecoveryReport {
|
|
|
827
1323
|
}
|
|
828
1324
|
/**
|
|
829
1325
|
* Reconcile `view.pending` in place. Returns the report + whether the
|
|
830
|
-
* view changed (caller
|
|
1326
|
+
* view changed (the caller writes once at the end — a crash during
|
|
831
1327
|
* recovery just means the same idempotent pass reruns next start).
|
|
832
1328
|
*/
|
|
833
1329
|
declare function recoverPendingOperations(view: StateView, client: RevokeClient, opts?: {
|
|
@@ -877,33 +1373,29 @@ interface ResolveOptions {
|
|
|
877
1373
|
}
|
|
878
1374
|
declare function resolveStateDir(opts?: ResolveOptions): PathResolution;
|
|
879
1375
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
*/
|
|
897
|
-
declare
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
type PassphraseSource = 'keychain' | 'env';
|
|
901
|
-
interface ResolvedPassphrase {
|
|
902
|
-
readonly passphrase: string;
|
|
903
|
-
readonly source: PassphraseSource;
|
|
1376
|
+
declare const SEED_ENV_VAR = "SEAL_SEED";
|
|
1377
|
+
declare const EPHEMERAL_ENV_VAR = "SEAL_MCP_EPHEMERAL";
|
|
1378
|
+
/** 0.2.x only: read once, to move a state of that version. */
|
|
1379
|
+
declare const PASSPHRASE_ENV_VAR = "SEAL_MCP_PASSPHRASE";
|
|
1380
|
+
declare const SEED_FILE_NAME = "seed";
|
|
1381
|
+
declare const KEYCHAIN_SERVICE = "seal-mcp";
|
|
1382
|
+
declare const KEYCHAIN_ACCOUNT = "seed";
|
|
1383
|
+
type SeedSource = 'env' | 'keychain' | 'file';
|
|
1384
|
+
interface RootSecret {
|
|
1385
|
+
readonly seed: Uint8Array;
|
|
1386
|
+
readonly source: SeedSource;
|
|
1387
|
+
}
|
|
1388
|
+
declare class SeedInvalidError extends Error {
|
|
1389
|
+
readonly code = "seed_invalid";
|
|
1390
|
+
constructor(where: string);
|
|
1391
|
+
}
|
|
1392
|
+
/** The state belongs to another key (contract `root_secret.identity_conflict`). */
|
|
1393
|
+
declare class IdentityConflictError extends Error {
|
|
1394
|
+
readonly code = "identity_conflict";
|
|
1395
|
+
constructor(statePath: string);
|
|
904
1396
|
}
|
|
905
1397
|
/**
|
|
906
|
-
* Surface used by
|
|
1398
|
+
* Surface used by this module — `keytar`'s shape, kept narrow on purpose
|
|
907
1399
|
* so tests can stub the keychain without pulling in the native module.
|
|
908
1400
|
*/
|
|
909
1401
|
interface KeytarLike {
|
|
@@ -911,43 +1403,35 @@ interface KeytarLike {
|
|
|
911
1403
|
setPassword(service: string, account: string, password: string): Promise<void>;
|
|
912
1404
|
deletePassword(service: string, account: string): Promise<boolean>;
|
|
913
1405
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
readonly
|
|
920
|
-
|
|
1406
|
+
/** The seed from the first source that has one; `null` when none has. */
|
|
1407
|
+
declare function readSeed(stateDir: string, env?: NodeJS.ProcessEnv): Promise<RootSecret | null>;
|
|
1408
|
+
/** The state passphrase: base64url(HKDF-SHA256(seed, 'seal-mcp:state:v1', 'passphrase', 32 bytes)). */
|
|
1409
|
+
declare function statePassphrase(seed: Uint8Array): string;
|
|
1410
|
+
interface AgentState {
|
|
1411
|
+
readonly root: RootSecret;
|
|
1412
|
+
readonly passphrase: string;
|
|
1413
|
+
/** This call made the state (a first start). */
|
|
1414
|
+
readonly created: boolean;
|
|
1415
|
+
/** This call moved a 0.2.x state under the seed: where that version's passphrase came from. */
|
|
1416
|
+
readonly moved: 'keychain' | 'env' | null;
|
|
921
1417
|
}
|
|
922
1418
|
/**
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
|
|
927
|
-
declare function resolvePassphraseFromStores(env?: NodeJS.ProcessEnv): Promise<ResolvedPassphrase | null>;
|
|
928
|
-
/**
|
|
929
|
-
* Probe whether the OS keychain is usable AND whether the seal-mcp
|
|
930
|
-
* entry already exists. Used by `sealnet-mcp doctor`.
|
|
1419
|
+
* The agent's state, opened with its root secret: made on the first start
|
|
1420
|
+
* (with the seed, when there is none), moved once from 0.2.x (SPEC-ADDRESS
|
|
1421
|
+
* F1 §3), refused when it belongs to another key. Under the state lock, so
|
|
1422
|
+
* two hosts starting together make one seed and one state.
|
|
931
1423
|
*/
|
|
1424
|
+
declare function openAgentState(path: PathResolution, env?: NodeJS.ProcessEnv): Promise<AgentState>;
|
|
1425
|
+
/** `reset`: the seed file and the keychain entries (the seed, and 0.2.x's passphrase). */
|
|
1426
|
+
declare function deleteRootSecret(stateDir: string): Promise<{
|
|
1427
|
+
seedFile: boolean;
|
|
1428
|
+
keychain: boolean;
|
|
1429
|
+
}>;
|
|
1430
|
+
/** `doctor`: is there a keychain, and does it hold the seed. */
|
|
932
1431
|
declare function probeKeychain(): Promise<{
|
|
933
1432
|
available: boolean;
|
|
934
|
-
|
|
1433
|
+
hasSeed: boolean;
|
|
935
1434
|
}>;
|
|
936
|
-
declare function setKeychainPassphrase(passphrase: string): Promise<void>;
|
|
937
|
-
declare function deleteKeychainPassphrase(): Promise<boolean>;
|
|
938
|
-
/**
|
|
939
|
-
* Prompt for a passphrase on the TTY without echoing keystrokes.
|
|
940
|
-
*
|
|
941
|
-
* Requires `process.stdin.isTTY === true` — caller must verify ahead
|
|
942
|
-
* of time and route non-TTY callers to the env-var path.
|
|
943
|
-
*
|
|
944
|
-
* Special keys:
|
|
945
|
-
* - `\r` / `\n` → resolve with the accumulated buffer.
|
|
946
|
-
* - `\u0003` (Ctrl-C) → reject with `cancelled`.
|
|
947
|
-
* - `\u0004` (Ctrl-D) on empty buffer → reject with `cancelled`.
|
|
948
|
-
* - `\u007f` / `\b` (Backspace) → delete last char.
|
|
949
|
-
*/
|
|
950
|
-
declare function promptHiddenPassphrase(prompt: string): Promise<string>;
|
|
951
1435
|
|
|
952
1436
|
/**
|
|
953
1437
|
* Shared `ToolContext` and helpers used by every tool implementation.
|
|
@@ -959,20 +1443,20 @@ declare function promptHiddenPassphrase(prompt: string): Promise<string>;
|
|
|
959
1443
|
* context) and prevents accidental global state.
|
|
960
1444
|
*/
|
|
961
1445
|
|
|
962
|
-
/**
|
|
1446
|
+
/** What every tool call gets. */
|
|
963
1447
|
interface ToolContext {
|
|
1448
|
+
/** The state as this process last read or wrote it; `update` replaces it. */
|
|
964
1449
|
readonly state: StateView;
|
|
965
1450
|
readonly identity: Identity;
|
|
966
|
-
readonly client:
|
|
1451
|
+
readonly client: SealApi;
|
|
967
1452
|
/**
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
* structured MCP error to the model.
|
|
1453
|
+
* The one way a tool changes the state (`state.ts::updateState`): the file
|
|
1454
|
+
* is read anew under the lock, `mutate` applied and saved, and `state`
|
|
1455
|
+
* becomes the result. Other MCP hosts on the machine write the same file,
|
|
1456
|
+
* so a tool never mutates `state` itself. Errors propagate to the tool.
|
|
973
1457
|
*/
|
|
974
|
-
|
|
975
|
-
/**
|
|
1458
|
+
readonly update: (mutate: StateMutation) => Promise<void>;
|
|
1459
|
+
/** `serve --ephemeral`, nothing on disk: handles die with the process (receipt says so). */
|
|
976
1460
|
readonly ephemeral?: boolean;
|
|
977
1461
|
/** `seal_request` handles waiting on a link, to their intake id; in memory only. */
|
|
978
1462
|
readonly requests?: Map<string, string>;
|
|
@@ -1320,4 +1804,4 @@ interface SealShareDeps {
|
|
|
1320
1804
|
}
|
|
1321
1805
|
declare function sealShare(ctx: ToolContext, input: SealShareInput, deps?: SealShareDeps): Promise<SealShareOutput>;
|
|
1322
1806
|
|
|
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,
|
|
1807
|
+
export { APP_NAME, type AcquireLockDeps, type AgentState, type AuditEntry, BACKEND_URL_ENV_VAR, BackendError, DEFAULT_BACKEND_URL, DEFAULT_PUBLIC_HOST, EPHEMERAL_ENV_VAR, type HandleEntry, HandoffBothChannelsFailedError, type HandoffPayload, type HandoffResult, type Identity, IdentityConflictError, type IdentityFields, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE, type KeytarLike, LOCK_FILE_NAME, type LockHolderInfo, type McpServerHandle, type McpServerOptions, PASSPHRASE_ENV_VAR, PUBLIC_HOST_ENV_VAR, type PathResolution, type PendingCompensation, type PendingOperation, type PendingStep, type RecoveryReport, type ResolveOptions, type RevokeClient, type RootSecret, SEED_ENV_VAR, SEED_FILE_NAME, 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, SeedInvalidError, type SeedSource, StateBusyError, StateCorrupt, type StateDirSource, type StateLock, StateLockedError, type StateMutation, StatePassphraseInvalid, type StateView, ToolError, acquireStateLock, createEmptyState, createServer, deleteRootSecret, generateHandle, generateIdentity, holderMayRun, identityFromSeed, identityToFields, isValidHandle, loadIdentity, loadState, openAgentState, performHandoff, probeKeychain, probeStateLock, readSeed, recoverPendingOperations, resolveBackendUrl, resolvePublicHost, resolveStateDir, saveState, sealList, sealOpen, sealRevoke, sealShare, statePassphrase, updateState, withStateLock };
|