@rndmjoker/proton-mcp 0.4.13 → 0.5.1
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 +138 -21
- package/dist/browser/channel.d.ts +76 -0
- package/dist/browser/channel.js +162 -0
- package/dist/browser/channel.js.map +1 -0
- package/dist/browser/client.d.ts +50 -0
- package/dist/browser/client.js +284 -0
- package/dist/browser/client.js.map +1 -0
- package/dist/browser/connector.d.ts +15 -0
- package/dist/browser/connector.js +26 -0
- package/dist/browser/connector.js.map +1 -0
- package/dist/browser/host.d.ts +50 -0
- package/dist/browser/host.js +258 -0
- package/dist/browser/host.js.map +1 -0
- package/dist/browser/messages.d.ts +163 -0
- package/dist/browser/messages.js +338 -0
- package/dist/browser/messages.js.map +1 -0
- package/dist/browser/paths.d.ts +51 -0
- package/dist/browser/paths.js +92 -0
- package/dist/browser/paths.js.map +1 -0
- package/dist/browser/setup.d.ts +42 -0
- package/dist/browser/setup.js +170 -0
- package/dist/browser/setup.js.map +1 -0
- package/dist/filters/order.d.ts +66 -0
- package/dist/filters/order.js +108 -0
- package/dist/filters/order.js.map +1 -0
- package/dist/filters/sieve.d.ts +184 -0
- package/dist/filters/sieve.js +1312 -0
- package/dist/filters/sieve.js.map +1 -0
- package/dist/instances.d.ts +58 -0
- package/dist/instances.js +145 -0
- package/dist/instances.js.map +1 -0
- package/dist/mail/compose.d.ts +10 -0
- package/dist/mail/compose.js.map +1 -1
- package/dist/mail/record.d.ts +36 -0
- package/dist/mail/record.js +74 -0
- package/dist/mail/record.js.map +1 -0
- package/dist/permissions.d.ts +139 -0
- package/dist/permissions.js +355 -0
- package/dist/permissions.js.map +1 -0
- package/dist/server.js +330 -16
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +12 -0
- package/dist/session.js +23 -0
- package/dist/session.js.map +1 -1
- package/dist/settings.d.ts +81 -11
- package/dist/settings.js +160 -25
- package/dist/settings.js.map +1 -1
- package/dist/tools/confirm.d.ts +4 -2
- package/dist/tools/confirm.js +12 -3
- package/dist/tools/confirm.js.map +1 -1
- package/dist/tools/failures.d.ts +6 -8
- package/dist/tools/failures.js +36 -17
- package/dist/tools/failures.js.map +1 -1
- package/dist/tools/filter-tools.d.ts +82 -0
- package/dist/tools/filter-tools.js +729 -0
- package/dist/tools/filter-tools.js.map +1 -0
- package/dist/tools/filters.d.ts +8 -1
- package/dist/tools/filters.js +24 -12
- package/dist/tools/filters.js.map +1 -1
- package/dist/tools/format.d.ts +15 -0
- package/dist/tools/format.js +4 -3
- package/dist/tools/format.js.map +1 -1
- package/dist/tools/guidance.js +12 -7
- package/dist/tools/guidance.js.map +1 -1
- package/dist/tools/interface.d.ts +20 -6
- package/dist/tools/interface.js +40 -16
- package/dist/tools/interface.js.map +1 -1
- package/dist/tools/send.d.ts +9 -0
- package/dist/tools/send.js +51 -9
- package/dist/tools/send.js.map +1 -1
- package/dist/tools/sign-in-required.d.ts +15 -2
- package/dist/tools/sign-in-required.js +30 -2
- package/dist/tools/sign-in-required.js.map +1 -1
- package/dist/web/layout.d.ts +12 -5
- package/dist/web/layout.js +32 -9
- package/dist/web/layout.js.map +1 -1
- package/dist/web/pages.d.ts +4 -2
- package/dist/web/pages.js +10 -9
- package/dist/web/pages.js.map +1 -1
- package/dist/web/pending.d.ts +9 -6
- package/dist/web/pending.js +9 -7
- package/dist/web/pending.js.map +1 -1
- package/dist/web/preview-server.d.ts +33 -0
- package/dist/web/preview-server.js +102 -0
- package/dist/web/preview-server.js.map +1 -0
- package/dist/web/sections.d.ts +86 -1
- package/dist/web/sections.js +290 -7
- package/dist/web/sections.js.map +1 -1
- package/dist/web/security.d.ts +48 -13
- package/dist/web/security.js +68 -16
- package/dist/web/security.js.map +1 -1
- package/dist/web/server.d.ts +81 -24
- package/dist/web/server.js +478 -117
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/web/security.d.ts
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The threat model is not "someone on the internet": the server binds to
|
|
5
5
|
* loopback only, so nobody reaches it from outside. What has to be defended
|
|
6
|
-
* against is anything
|
|
7
|
-
*
|
|
6
|
+
* against is anything the user's browser is talked into doing by a page from
|
|
7
|
+
* elsewhere, and, when the user sets one, anything on the same machine that does
|
|
8
|
+
* not know the PIN.
|
|
8
9
|
*
|
|
9
10
|
* Four defences, each against a specific attack:
|
|
10
11
|
*
|
|
11
|
-
* 1. **
|
|
12
|
-
* 127.0.0.1
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 1. **An optional PIN on every change.** Any local program can send a request
|
|
13
|
+
* to 127.0.0.1, and so can the assistant, through a browser tool. Until
|
|
14
|
+
* 2026-11-05 a token in the address kept them out, but the address reached
|
|
15
|
+
* the assistant through `open_configuration`, so the token protected against
|
|
16
|
+
* everything except the one party the switches for autonomy are about. A PIN
|
|
17
|
+
* the user types and the assistant never sees does. It is optional: without
|
|
18
|
+
* one, anything on this machine can change the settings, and the pages say so.
|
|
15
19
|
* 2. **Host header check.** A page on the internet can resolve its own domain to
|
|
16
20
|
* 127.0.0.1 and have the browser send requests there. The browser then treats
|
|
17
21
|
* that domain as the origin, so same-origin rules do not help. Rejecting any
|
|
@@ -30,13 +34,44 @@
|
|
|
30
34
|
*/
|
|
31
35
|
import type { IncomingMessage } from 'node:http';
|
|
32
36
|
export interface SessionSecrets {
|
|
33
|
-
/** Grants access at all. Part of the URL the user opens. */
|
|
34
|
-
accessToken: string;
|
|
35
37
|
/** Signs CSRF tokens. Never leaves the process. */
|
|
36
38
|
csrfSecret: string;
|
|
37
39
|
}
|
|
38
40
|
/** Creates the per-process secrets. */
|
|
39
41
|
export declare function createSession(): SessionSecrets;
|
|
42
|
+
/**
|
|
43
|
+
* A PIN as it is stored: salted and run through scrypt, never the PIN itself.
|
|
44
|
+
*
|
|
45
|
+
* The settings file is readable by the user's own programs, the assistant
|
|
46
|
+
* included when it can read files. A plain PIN there would be a PIN the
|
|
47
|
+
* assistant can look up. A hash of a short PIN can still be guessed offline,
|
|
48
|
+
* which is why the length rule below asks for more than four digits and why
|
|
49
|
+
* scrypt rather than a fast hash: every guess has to pay for the memory.
|
|
50
|
+
*/
|
|
51
|
+
export interface StoredPin {
|
|
52
|
+
salt: string;
|
|
53
|
+
hash: string;
|
|
54
|
+
}
|
|
55
|
+
/** Why a PIN cannot be used, or nothing when it can. */
|
|
56
|
+
export declare function pinProblem(pin: string): string | undefined;
|
|
57
|
+
export declare function hashPin(pin: string): Promise<StoredPin>;
|
|
58
|
+
export declare function pinMatches(pin: string, stored: StoredPin): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Slows down guessing.
|
|
61
|
+
*
|
|
62
|
+
* After `max` wrong PINs within `windowMs`, every attempt is refused until the
|
|
63
|
+
* window has passed, the right PIN included: a lock that the right answer opens
|
|
64
|
+
* early tells the guesser when they found it. Held in memory by the server that
|
|
65
|
+
* shows the interface, which is the only one that checks PINs.
|
|
66
|
+
*/
|
|
67
|
+
export declare class AttemptLimiter {
|
|
68
|
+
#private;
|
|
69
|
+
constructor(max?: number, windowMs?: number);
|
|
70
|
+
/** Whether an attempt may be checked at all right now. */
|
|
71
|
+
allowed(now?: number): boolean;
|
|
72
|
+
fail(now?: number): void;
|
|
73
|
+
reset(): void;
|
|
74
|
+
}
|
|
40
75
|
/** Compares two secrets without leaking their similarity through timing. */
|
|
41
76
|
export declare function safeEqual(a: string, b: string): boolean;
|
|
42
77
|
/**
|
|
@@ -64,7 +99,7 @@ export declare function isAllowedOrigin(origin: string | undefined, port: number
|
|
|
64
99
|
export declare function csrfToken(secrets: SessionSecrets, purpose: string): string;
|
|
65
100
|
export declare function csrfValid(secrets: SessionSecrets, purpose: string, presented: string): boolean;
|
|
66
101
|
/** Why a request was rejected. Deliberately vague towards the client. */
|
|
67
|
-
export type RejectionReason = 'bad-host' | 'bad-origin' | 'bad-
|
|
102
|
+
export type RejectionReason = 'bad-host' | 'bad-origin' | 'bad-csrf' | 'method-not-allowed';
|
|
68
103
|
export interface CheckResult {
|
|
69
104
|
ok: boolean;
|
|
70
105
|
reason?: RejectionReason;
|
|
@@ -73,8 +108,6 @@ export interface CheckOptions {
|
|
|
73
108
|
request: IncomingMessage;
|
|
74
109
|
secrets: SessionSecrets;
|
|
75
110
|
port: number;
|
|
76
|
-
/** The token presented by the client, from the query string or a header. */
|
|
77
|
-
presentedToken: string | undefined;
|
|
78
111
|
/** True for requests that change something. */
|
|
79
112
|
writing: boolean;
|
|
80
113
|
/** For writing requests: the CSRF token and what it is for. */
|
|
@@ -86,8 +119,10 @@ export interface CheckOptions {
|
|
|
86
119
|
/**
|
|
87
120
|
* Runs every check in order.
|
|
88
121
|
*
|
|
89
|
-
* Order matters: the host check comes
|
|
90
|
-
*
|
|
122
|
+
* Order matters: the host check comes first, so a rebinding attempt is refused
|
|
123
|
+
* before anything else is looked at. The PIN is not checked here: a wrong PIN is
|
|
124
|
+
* the user's mistake and gets a page that says so, where every failure in here
|
|
125
|
+
* gets the same refusal that says nothing.
|
|
91
126
|
*/
|
|
92
127
|
export declare function checkRequest(options: CheckOptions): CheckResult;
|
|
93
128
|
/**
|
package/dist/web/security.js
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The threat model is not "someone on the internet": the server binds to
|
|
5
5
|
* loopback only, so nobody reaches it from outside. What has to be defended
|
|
6
|
-
* against is anything
|
|
7
|
-
*
|
|
6
|
+
* against is anything the user's browser is talked into doing by a page from
|
|
7
|
+
* elsewhere, and, when the user sets one, anything on the same machine that does
|
|
8
|
+
* not know the PIN.
|
|
8
9
|
*
|
|
9
10
|
* Four defences, each against a specific attack:
|
|
10
11
|
*
|
|
11
|
-
* 1. **
|
|
12
|
-
* 127.0.0.1
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 1. **An optional PIN on every change.** Any local program can send a request
|
|
13
|
+
* to 127.0.0.1, and so can the assistant, through a browser tool. Until
|
|
14
|
+
* 2026-11-05 a token in the address kept them out, but the address reached
|
|
15
|
+
* the assistant through `open_configuration`, so the token protected against
|
|
16
|
+
* everything except the one party the switches for autonomy are about. A PIN
|
|
17
|
+
* the user types and the assistant never sees does. It is optional: without
|
|
18
|
+
* one, anything on this machine can change the settings, and the pages say so.
|
|
15
19
|
* 2. **Host header check.** A page on the internet can resolve its own domain to
|
|
16
20
|
* 127.0.0.1 and have the browser send requests there. The browser then treats
|
|
17
21
|
* that domain as the origin, so same-origin rules do not help. Rejecting any
|
|
@@ -28,18 +32,67 @@
|
|
|
28
32
|
* All comparisons of secrets use timingSafeEqual, so response time does not
|
|
29
33
|
* reveal how much of a token was right.
|
|
30
34
|
*/
|
|
31
|
-
import { randomBytes, timingSafeEqual, createHmac } from 'node:crypto';
|
|
35
|
+
import { randomBytes, timingSafeEqual, createHmac, scrypt } from 'node:crypto';
|
|
32
36
|
/** Hostnames that mean this machine. Anything else is rejected. */
|
|
33
37
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]', '::1']);
|
|
34
38
|
/** Creates the per-process secrets. */
|
|
35
39
|
export function createSession() {
|
|
36
40
|
return {
|
|
37
|
-
// 32 bytes base64url: long enough that guessing is hopeless, short enough
|
|
38
|
-
// to survive being copied into a browser.
|
|
39
|
-
accessToken: randomBytes(32).toString('base64url'),
|
|
40
41
|
csrfSecret: randomBytes(32).toString('base64url'),
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
/** scrypt with Node's default cost, named so a change here is deliberate. */
|
|
45
|
+
const PIN_KEY_LENGTH = 32;
|
|
46
|
+
const PIN_COST = { N: 16384, r: 8, p: 1 };
|
|
47
|
+
function derive(pin, salt) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
scrypt(pin.normalize('NFC'), salt, PIN_KEY_LENGTH, PIN_COST, (error, key) => error ? reject(error) : resolve(key));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Why a PIN cannot be used, or nothing when it can. */
|
|
53
|
+
export function pinProblem(pin) {
|
|
54
|
+
if (pin.length < 6)
|
|
55
|
+
return 'The PIN needs at least 6 characters.';
|
|
56
|
+
if (pin.length > 128)
|
|
57
|
+
return 'The PIN can have at most 128 characters.';
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
export async function hashPin(pin) {
|
|
61
|
+
const salt = randomBytes(16).toString('base64url');
|
|
62
|
+
return { salt, hash: (await derive(pin, salt)).toString('base64url') };
|
|
63
|
+
}
|
|
64
|
+
export async function pinMatches(pin, stored) {
|
|
65
|
+
const presented = await derive(pin, stored.salt);
|
|
66
|
+
return safeEqual(presented.toString('base64url'), stored.hash);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Slows down guessing.
|
|
70
|
+
*
|
|
71
|
+
* After `max` wrong PINs within `windowMs`, every attempt is refused until the
|
|
72
|
+
* window has passed, the right PIN included: a lock that the right answer opens
|
|
73
|
+
* early tells the guesser when they found it. Held in memory by the server that
|
|
74
|
+
* shows the interface, which is the only one that checks PINs.
|
|
75
|
+
*/
|
|
76
|
+
export class AttemptLimiter {
|
|
77
|
+
#failures = [];
|
|
78
|
+
#max;
|
|
79
|
+
#windowMs;
|
|
80
|
+
constructor(max = 5, windowMs = 60_000) {
|
|
81
|
+
this.#max = max;
|
|
82
|
+
this.#windowMs = windowMs;
|
|
83
|
+
}
|
|
84
|
+
/** Whether an attempt may be checked at all right now. */
|
|
85
|
+
allowed(now = Date.now()) {
|
|
86
|
+
this.#failures = this.#failures.filter((at) => now - at < this.#windowMs);
|
|
87
|
+
return this.#failures.length < this.#max;
|
|
88
|
+
}
|
|
89
|
+
fail(now = Date.now()) {
|
|
90
|
+
this.#failures.push(now);
|
|
91
|
+
}
|
|
92
|
+
reset() {
|
|
93
|
+
this.#failures = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
43
96
|
/** Compares two secrets without leaking their similarity through timing. */
|
|
44
97
|
export function safeEqual(a, b) {
|
|
45
98
|
const left = Buffer.from(a, 'utf8');
|
|
@@ -109,16 +162,15 @@ export function csrfValid(secrets, purpose, presented) {
|
|
|
109
162
|
/**
|
|
110
163
|
* Runs every check in order.
|
|
111
164
|
*
|
|
112
|
-
* Order matters: the host check comes
|
|
113
|
-
*
|
|
165
|
+
* Order matters: the host check comes first, so a rebinding attempt is refused
|
|
166
|
+
* before anything else is looked at. The PIN is not checked here: a wrong PIN is
|
|
167
|
+
* the user's mistake and gets a page that says so, where every failure in here
|
|
168
|
+
* gets the same refusal that says nothing.
|
|
114
169
|
*/
|
|
115
170
|
export function checkRequest(options) {
|
|
116
|
-
const { request, secrets, port,
|
|
171
|
+
const { request, secrets, port, writing, csrf } = options;
|
|
117
172
|
if (!isAllowedHost(request.headers.host))
|
|
118
173
|
return { ok: false, reason: 'bad-host' };
|
|
119
|
-
if (!presentedToken || !safeEqual(secrets.accessToken, presentedToken)) {
|
|
120
|
-
return { ok: false, reason: 'bad-token' };
|
|
121
|
-
}
|
|
122
174
|
if (writing) {
|
|
123
175
|
if (!isAllowedOrigin(request.headers.origin, port))
|
|
124
176
|
return { ok: false, reason: 'bad-origin' };
|
package/dist/web/security.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/web/security.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/web/security.ts"],"names":[],"mappings":"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"}
|
package/dist/web/server.d.ts
CHANGED
|
@@ -12,30 +12,68 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type IncomingMessage } from 'node:http';
|
|
14
14
|
import type { Credentials, CredentialStore, StoreKind } from '../credentials/store.js';
|
|
15
|
-
import { type
|
|
15
|
+
import { type StoredPin } from './security.js';
|
|
16
|
+
import { type ServerSummary, type ToolLevelView, type LimitsView, type RecordView, type FilterActionsView } from './sections.js';
|
|
17
|
+
import type { Level, SendLimits, RecordCopy } from '../settings.js';
|
|
18
|
+
import { type Unlockable } from '../filters/sieve.js';
|
|
16
19
|
/**
|
|
17
|
-
* The
|
|
20
|
+
* The one address of the interface, shared by every running server.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* Fixed rather than random, and since 2026-11-05 also not walked upwards when
|
|
23
|
+
* taken: every client session starts its own server, and the interface they
|
|
24
|
+
* share has to be at the same address for all of them. The first server to
|
|
25
|
+
* start serves it; the others take over when it stops.
|
|
22
26
|
*/
|
|
23
27
|
export declare const DEFAULT_WEB_PORT = 7345;
|
|
28
|
+
/** How often a server that does not serve the interface tries to take it over. */
|
|
29
|
+
export declare const TAKEOVER_INTERVAL_MS = 10000;
|
|
24
30
|
/**
|
|
25
|
-
*
|
|
31
|
+
* Who is on the port: another proton-mcp, an older one, something else, or
|
|
32
|
+
* nobody.
|
|
26
33
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* "older" is a proton-mcp from before the shared interface, 0.4 and earlier.
|
|
35
|
+
* Each of those served an interface of its own, with a token in the address,
|
|
36
|
+
* and knows nothing of the others. One still running in a session started
|
|
37
|
+
* before an update holds the port until that session ends.
|
|
30
38
|
*/
|
|
31
|
-
export
|
|
39
|
+
export type PortOccupant = 'proton-mcp' | 'older' | 'other' | 'none';
|
|
32
40
|
export interface WebInterfaceOptions {
|
|
33
41
|
/**
|
|
34
|
-
* Left out, the interface
|
|
35
|
-
*
|
|
36
|
-
*
|
|
42
|
+
* Left out, the interface uses DEFAULT_WEB_PORT. 0 lets the operating system
|
|
43
|
+
* pick a free port, which the tests use to avoid collisions; there is no
|
|
44
|
+
* takeover then, since no other server could know the address.
|
|
37
45
|
*/
|
|
38
46
|
port?: number;
|
|
47
|
+
/** The PIN as stored, or nothing while none is set. Read on every request. */
|
|
48
|
+
getPin?: () => StoredPin | undefined;
|
|
49
|
+
/** Stores a new PIN, or removes it with undefined. Returns an error to show. */
|
|
50
|
+
onPinChange?: (next: StoredPin | undefined) => Promise<string | undefined>;
|
|
51
|
+
/** Every running server, for the overview. */
|
|
52
|
+
getServers?: () => ServerSummary[];
|
|
53
|
+
/** Told whenever this server starts or stops serving the interface. */
|
|
54
|
+
onServingChange?: (serving: boolean) => void;
|
|
55
|
+
/** How often to try taking over the port. TAKEOVER_INTERVAL_MS unless a test says otherwise. */
|
|
56
|
+
takeoverIntervalMs?: number;
|
|
57
|
+
/** What each tool may do and the bounds on sending without asking, for the permissions page. */
|
|
58
|
+
getPermissions?: () => {
|
|
59
|
+
tools: ToolLevelView[];
|
|
60
|
+
limits: LimitsView;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Stores the levels chosen on the page, only those not fixed by the
|
|
64
|
+
* environment. Returns an error to show, or nothing.
|
|
65
|
+
*/
|
|
66
|
+
onLevels?: (levels: Record<string, Level>) => Promise<string | undefined>;
|
|
67
|
+
/** Stores the bounds on sending without asking. Returns an error to show, or nothing. */
|
|
68
|
+
onLimits?: (limits: SendLimits) => Promise<string | undefined>;
|
|
69
|
+
/** The recipient added to every message sent, for the pages. */
|
|
70
|
+
getRecord?: () => RecordView;
|
|
71
|
+
/** Stores it, or removes it with undefined. Returns an error to show, or nothing. */
|
|
72
|
+
onRecord?: (record: RecordCopy | undefined) => Promise<string | undefined>;
|
|
73
|
+
/** What a filter from an assistant may do beyond filing and flags, for the pages. */
|
|
74
|
+
getFilterActions?: () => FilterActionsView;
|
|
75
|
+
/** Stores it. Returns an error to show, or nothing. */
|
|
76
|
+
onFilterActions?: (actions: Unlockable[]) => Promise<string | undefined>;
|
|
39
77
|
/**
|
|
40
78
|
* Called when the user signs in. Returns an error message to show, or nothing
|
|
41
79
|
* on success. The master password is only set for the encrypted-file store.
|
|
@@ -110,14 +148,6 @@ export interface WebInterfaceOptions {
|
|
|
110
148
|
noticeDismissed?: () => boolean;
|
|
111
149
|
/** Remembers a dismissal. Without it the notice has no button. */
|
|
112
150
|
onDismissNotice?: () => Promise<void>;
|
|
113
|
-
/**
|
|
114
|
-
* The message behind a pending confirmation, if one is waiting.
|
|
115
|
-
*
|
|
116
|
-
* Absent when nothing can be previewed, which makes the page say so rather
|
|
117
|
-
* than fail. What it returns is decrypted mail, so the caller holds it in
|
|
118
|
-
* memory only and drops it as soon as the question is answered.
|
|
119
|
-
*/
|
|
120
|
-
getPending?: (digest: string) => Omit<PendingData, 'token' | 'disclaimer'> | undefined;
|
|
121
151
|
/** Where notices go. stderr, never stdout, which carries the MCP protocol. */
|
|
122
152
|
notify?: (message: string) => void;
|
|
123
153
|
}
|
|
@@ -165,13 +195,40 @@ export declare function readBody(request: IncomingMessage, maxBytes?: number): P
|
|
|
165
195
|
export declare class WebInterface {
|
|
166
196
|
#private;
|
|
167
197
|
constructor(options: WebInterfaceOptions);
|
|
168
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* The address of the interface. The same for every running server, whichever
|
|
200
|
+
* of them serves it, and nothing in it is secret.
|
|
201
|
+
*/
|
|
169
202
|
get url(): string;
|
|
170
203
|
get port(): number;
|
|
204
|
+
/** True while this server serves the interface itself. */
|
|
171
205
|
get running(): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Whether the interface can be reached at `url`, from this server or from
|
|
208
|
+
* another running proton-mcp. False when something else holds the port.
|
|
209
|
+
*/
|
|
210
|
+
get reachable(): boolean;
|
|
211
|
+
/** Who held the port when this server last found it taken. */
|
|
212
|
+
get occupant(): PortOccupant;
|
|
213
|
+
/**
|
|
214
|
+
* Asks whoever holds the port whether it is a proton-mcp.
|
|
215
|
+
*
|
|
216
|
+
* The answer decides between "another session serves the interface, all is
|
|
217
|
+
* well" and "something else sits on this port", which the user has to hear
|
|
218
|
+
* about, because then nobody serves the interface at all. An older proton-mcp
|
|
219
|
+
* is the third answer: no interface either, but the remedy is to restart the
|
|
220
|
+
* sessions that still run it, not to move every server to another port.
|
|
221
|
+
*/
|
|
222
|
+
static probe(port: number, timeoutMs?: number): Promise<PortOccupant>;
|
|
223
|
+
/**
|
|
224
|
+
* Serves the interface, or leaves it to the server that already does.
|
|
225
|
+
*
|
|
226
|
+
* Never throws over a taken port. Another proton-mcp on it is the ordinary
|
|
227
|
+
* case with several sessions; this server then tries every few seconds to
|
|
228
|
+
* take over, so the interface survives the session that started it. Anything
|
|
229
|
+
* else on the port is reported once, and taken over too if it ever leaves.
|
|
230
|
+
*/
|
|
172
231
|
start(): Promise<string>;
|
|
173
232
|
stop(): Promise<void>;
|
|
174
|
-
/** Invalidates the current link and issues a new one. */
|
|
175
|
-
rotateToken(): string;
|
|
176
233
|
}
|
|
177
234
|
export type { CredentialStore };
|