@rndmjoker/proton-mcp 0.4.4

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.
Files changed (141) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +405 -0
  3. package/dist/activity.d.ts +49 -0
  4. package/dist/activity.js +72 -0
  5. package/dist/activity.js.map +1 -0
  6. package/dist/bridge/certificate.d.ts +75 -0
  7. package/dist/bridge/certificate.js +176 -0
  8. package/dist/bridge/certificate.js.map +1 -0
  9. package/dist/bridge/connection.d.ts +99 -0
  10. package/dist/bridge/connection.js +244 -0
  11. package/dist/bridge/connection.js.map +1 -0
  12. package/dist/bridge/errors.d.ts +35 -0
  13. package/dist/bridge/errors.js +56 -0
  14. package/dist/bridge/errors.js.map +1 -0
  15. package/dist/config.d.ts +92 -0
  16. package/dist/config.js +142 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/credentials/availability.d.ts +62 -0
  19. package/dist/credentials/availability.js +107 -0
  20. package/dist/credentials/availability.js.map +1 -0
  21. package/dist/credentials/encrypted-file.d.ts +50 -0
  22. package/dist/credentials/encrypted-file.js +156 -0
  23. package/dist/credentials/encrypted-file.js.map +1 -0
  24. package/dist/credentials/keyring.d.ts +48 -0
  25. package/dist/credentials/keyring.js +97 -0
  26. package/dist/credentials/keyring.js.map +1 -0
  27. package/dist/credentials/plain-file.d.ts +24 -0
  28. package/dist/credentials/plain-file.js +78 -0
  29. package/dist/credentials/plain-file.js.map +1 -0
  30. package/dist/credentials/probe-keyring.d.ts +11 -0
  31. package/dist/credentials/probe-keyring.js +57 -0
  32. package/dist/credentials/probe-keyring.js.map +1 -0
  33. package/dist/credentials/session.d.ts +15 -0
  34. package/dist/credentials/session.js +21 -0
  35. package/dist/credentials/session.js.map +1 -0
  36. package/dist/credentials/store.d.ts +80 -0
  37. package/dist/credentials/store.js +94 -0
  38. package/dist/credentials/store.js.map +1 -0
  39. package/dist/in-flight.d.ts +33 -0
  40. package/dist/in-flight.js +78 -0
  41. package/dist/in-flight.js.map +1 -0
  42. package/dist/mail/actions.d.ts +116 -0
  43. package/dist/mail/actions.js +265 -0
  44. package/dist/mail/actions.js.map +1 -0
  45. package/dist/mail/attachments.d.ts +39 -0
  46. package/dist/mail/attachments.js +86 -0
  47. package/dist/mail/attachments.js.map +1 -0
  48. package/dist/mail/compose.d.ts +264 -0
  49. package/dist/mail/compose.js +395 -0
  50. package/dist/mail/compose.js.map +1 -0
  51. package/dist/mail/drafts.d.ts +138 -0
  52. package/dist/mail/drafts.js +449 -0
  53. package/dist/mail/drafts.js.map +1 -0
  54. package/dist/mail/ids.d.ts +78 -0
  55. package/dist/mail/ids.js +135 -0
  56. package/dist/mail/ids.js.map +1 -0
  57. package/dist/mail/labels.d.ts +61 -0
  58. package/dist/mail/labels.js +139 -0
  59. package/dist/mail/labels.js.map +1 -0
  60. package/dist/mail/markup.d.ts +91 -0
  61. package/dist/mail/markup.js +265 -0
  62. package/dist/mail/markup.js.map +1 -0
  63. package/dist/mail/messages.d.ts +147 -0
  64. package/dist/mail/messages.js +219 -0
  65. package/dist/mail/messages.js.map +1 -0
  66. package/dist/mail/quote.d.ts +65 -0
  67. package/dist/mail/quote.js +157 -0
  68. package/dist/mail/quote.js.map +1 -0
  69. package/dist/mail/search.d.ts +66 -0
  70. package/dist/mail/search.js +107 -0
  71. package/dist/mail/search.js.map +1 -0
  72. package/dist/mail/send.d.ts +61 -0
  73. package/dist/mail/send.js +132 -0
  74. package/dist/mail/send.js.map +1 -0
  75. package/dist/mime/parse.d.ts +110 -0
  76. package/dist/mime/parse.js +194 -0
  77. package/dist/mime/parse.js.map +1 -0
  78. package/dist/server.d.ts +13 -0
  79. package/dist/server.js +387 -0
  80. package/dist/server.js.map +1 -0
  81. package/dist/session.d.ts +86 -0
  82. package/dist/session.js +220 -0
  83. package/dist/session.js.map +1 -0
  84. package/dist/settings.d.ts +52 -0
  85. package/dist/settings.js +80 -0
  86. package/dist/settings.js.map +1 -0
  87. package/dist/tools/actions.d.ts +21 -0
  88. package/dist/tools/actions.js +133 -0
  89. package/dist/tools/actions.js.map +1 -0
  90. package/dist/tools/capabilities.d.ts +52 -0
  91. package/dist/tools/capabilities.js +96 -0
  92. package/dist/tools/capabilities.js.map +1 -0
  93. package/dist/tools/confirm.d.ts +84 -0
  94. package/dist/tools/confirm.js +212 -0
  95. package/dist/tools/confirm.js.map +1 -0
  96. package/dist/tools/drafts.d.ts +21 -0
  97. package/dist/tools/drafts.js +199 -0
  98. package/dist/tools/drafts.js.map +1 -0
  99. package/dist/tools/failures.d.ts +46 -0
  100. package/dist/tools/failures.js +157 -0
  101. package/dist/tools/failures.js.map +1 -0
  102. package/dist/tools/format.d.ts +55 -0
  103. package/dist/tools/format.js +220 -0
  104. package/dist/tools/format.js.map +1 -0
  105. package/dist/tools/guidance.d.ts +38 -0
  106. package/dist/tools/guidance.js +226 -0
  107. package/dist/tools/guidance.js.map +1 -0
  108. package/dist/tools/interface.d.ts +28 -0
  109. package/dist/tools/interface.js +63 -0
  110. package/dist/tools/interface.js.map +1 -0
  111. package/dist/tools/labels.d.ts +21 -0
  112. package/dist/tools/labels.js +92 -0
  113. package/dist/tools/labels.js.map +1 -0
  114. package/dist/tools/list-folders.d.ts +14 -0
  115. package/dist/tools/list-folders.js +70 -0
  116. package/dist/tools/list-folders.js.map +1 -0
  117. package/dist/tools/messages.d.ts +9 -0
  118. package/dist/tools/messages.js +221 -0
  119. package/dist/tools/messages.js.map +1 -0
  120. package/dist/tools/send.d.ts +29 -0
  121. package/dist/tools/send.js +236 -0
  122. package/dist/tools/send.js.map +1 -0
  123. package/dist/tools/sign-in-required.d.ts +94 -0
  124. package/dist/tools/sign-in-required.js +168 -0
  125. package/dist/tools/sign-in-required.js.map +1 -0
  126. package/dist/web/layout.d.ts +75 -0
  127. package/dist/web/layout.js +590 -0
  128. package/dist/web/layout.js.map +1 -0
  129. package/dist/web/pages.d.ts +79 -0
  130. package/dist/web/pages.js +284 -0
  131. package/dist/web/pages.js.map +1 -0
  132. package/dist/web/sections.d.ts +105 -0
  133. package/dist/web/sections.js +207 -0
  134. package/dist/web/sections.js.map +1 -0
  135. package/dist/web/security.d.ts +122 -0
  136. package/dist/web/security.js +180 -0
  137. package/dist/web/security.js.map +1 -0
  138. package/dist/web/server.d.ts +153 -0
  139. package/dist/web/server.js +622 -0
  140. package/dist/web/server.js.map +1 -0
  141. package/package.json +70 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The security layer of the local web interface.
3
+ *
4
+ * The threat model is not "someone on the internet": the server binds to
5
+ * loopback only, so nobody reaches it from outside. What has to be defended
6
+ * against is anything else running on the same machine, and anything the user's
7
+ * browser is talked into doing by a page from elsewhere.
8
+ *
9
+ * Four defences, each against a specific attack:
10
+ *
11
+ * 1. **A session token in the URL.** Any local program can send a request to
12
+ * 127.0.0.1. Without a secret it could drive the interface, read the state
13
+ * and store credentials of its choosing. The token is generated per process
14
+ * and never written to disk.
15
+ * 2. **Host header check.** A page on the internet can resolve its own domain to
16
+ * 127.0.0.1 and have the browser send requests there. The browser then treats
17
+ * that domain as the origin, so same-origin rules do not help. Rejecting any
18
+ * Host other than a loopback name stops it. This is DNS rebinding, and it is
19
+ * the attack local servers most often forget.
20
+ * 3. **Origin check plus CSRF token on writes.** A page from elsewhere can post
21
+ * a form to 127.0.0.1 without reading the response. That is enough to change
22
+ * settings, so writes require a matching origin and a token bound to the
23
+ * session.
24
+ * 4. **A restrictive Content-Security-Policy.** No external scripts, styles,
25
+ * fonts or images, and no inline event handlers, so injected markup cannot
26
+ * fetch anything or run.
27
+ *
28
+ * All comparisons of secrets use timingSafeEqual, so response time does not
29
+ * reveal how much of a token was right.
30
+ */
31
+ import type { IncomingMessage } from 'node:http';
32
+ export interface SessionSecrets {
33
+ /** Grants access at all. Part of the URL the user opens. */
34
+ accessToken: string;
35
+ /** Signs CSRF tokens. Never leaves the process. */
36
+ csrfSecret: string;
37
+ }
38
+ /** Creates the per-process secrets. */
39
+ export declare function createSession(): SessionSecrets;
40
+ /** Compares two secrets without leaking their similarity through timing. */
41
+ export declare function safeEqual(a: string, b: string): boolean;
42
+ /**
43
+ * Checks the Host header.
44
+ *
45
+ * The defence against DNS rebinding: a page at evil.example.com can point that
46
+ * name at 127.0.0.1, and the browser will happily send requests with
47
+ * `Host: evil.example.com`. Only loopback names are accepted.
48
+ */
49
+ export declare function isAllowedHost(host: string | undefined): boolean;
50
+ /**
51
+ * Checks the Origin header for state-changing requests.
52
+ *
53
+ * A missing Origin is accepted, because browsers omit it for ordinary top-level
54
+ * navigation and non-browser clients never send it. A present but foreign one is
55
+ * rejected: that is a cross-site request.
56
+ */
57
+ export declare function isAllowedOrigin(origin: string | undefined, port: number): boolean;
58
+ /**
59
+ * Derives a CSRF token for a form.
60
+ *
61
+ * Bound to the session secret and to the form's purpose, so a token for one
62
+ * action cannot be replayed against another.
63
+ */
64
+ export declare function csrfToken(secrets: SessionSecrets, purpose: string): string;
65
+ export declare function csrfValid(secrets: SessionSecrets, purpose: string, presented: string): boolean;
66
+ /** Why a request was rejected. Deliberately vague towards the client. */
67
+ export type RejectionReason = 'bad-host' | 'bad-origin' | 'bad-token' | 'bad-csrf' | 'method-not-allowed';
68
+ export interface CheckResult {
69
+ ok: boolean;
70
+ reason?: RejectionReason;
71
+ }
72
+ export interface CheckOptions {
73
+ request: IncomingMessage;
74
+ secrets: SessionSecrets;
75
+ port: number;
76
+ /** The token presented by the client, from the query string or a header. */
77
+ presentedToken: string | undefined;
78
+ /** True for requests that change something. */
79
+ writing: boolean;
80
+ /** For writing requests: the CSRF token and what it is for. */
81
+ csrf?: {
82
+ purpose: string;
83
+ presented: string | undefined;
84
+ };
85
+ }
86
+ /**
87
+ * Runs every check in order.
88
+ *
89
+ * Order matters: the host check comes before the token check, so a rebinding
90
+ * attempt is refused before it can even try tokens.
91
+ */
92
+ export declare function checkRequest(options: CheckOptions): CheckResult;
93
+ /**
94
+ * The referrer policy.
95
+ *
96
+ * `same-origin` rather than the stricter-looking `no-referrer`, and this is not a
97
+ * concession: `no-referrer` **breaks the Origin check above**, which makes every
98
+ * form on this interface unusable.
99
+ *
100
+ * The mechanism, per the Fetch standard: when a request's referrer policy is
101
+ * `no-referrer`, the browser sets the request's origin to an opaque one, and the
102
+ * `Origin` header goes out as the literal string `null`. Since a foreign or
103
+ * sandboxed page can also produce `Origin: null`, that value has to be refused,
104
+ * so the interface would refuse its own forms. Confirmed in a real browser:
105
+ * every POST answered 404 with reason `bad-origin` until this was changed.
106
+ *
107
+ * Nothing is given up. The strict CSP already forbids this page from reaching any
108
+ * other host, and the page contains no external links, so `same-origin` has no
109
+ * third party to leak a referrer to in the first place.
110
+ *
111
+ * Do not "harden" this back to `no-referrer` without a browser to try it in.
112
+ */
113
+ export declare const REFERRER_POLICY = "same-origin";
114
+ /**
115
+ * The headers every response carries.
116
+ *
117
+ * The CSP is the strict kind: nothing may be loaded from anywhere, styles are
118
+ * allowed only from the document itself, and there is no way to reach the
119
+ * network. `frame-ancestors 'none'` keeps the page out of foreign frames, which
120
+ * also rules out clickjacking.
121
+ */
122
+ export declare function securityHeaders(): Record<string, string>;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The security layer of the local web interface.
3
+ *
4
+ * The threat model is not "someone on the internet": the server binds to
5
+ * loopback only, so nobody reaches it from outside. What has to be defended
6
+ * against is anything else running on the same machine, and anything the user's
7
+ * browser is talked into doing by a page from elsewhere.
8
+ *
9
+ * Four defences, each against a specific attack:
10
+ *
11
+ * 1. **A session token in the URL.** Any local program can send a request to
12
+ * 127.0.0.1. Without a secret it could drive the interface, read the state
13
+ * and store credentials of its choosing. The token is generated per process
14
+ * and never written to disk.
15
+ * 2. **Host header check.** A page on the internet can resolve its own domain to
16
+ * 127.0.0.1 and have the browser send requests there. The browser then treats
17
+ * that domain as the origin, so same-origin rules do not help. Rejecting any
18
+ * Host other than a loopback name stops it. This is DNS rebinding, and it is
19
+ * the attack local servers most often forget.
20
+ * 3. **Origin check plus CSRF token on writes.** A page from elsewhere can post
21
+ * a form to 127.0.0.1 without reading the response. That is enough to change
22
+ * settings, so writes require a matching origin and a token bound to the
23
+ * session.
24
+ * 4. **A restrictive Content-Security-Policy.** No external scripts, styles,
25
+ * fonts or images, and no inline event handlers, so injected markup cannot
26
+ * fetch anything or run.
27
+ *
28
+ * All comparisons of secrets use timingSafeEqual, so response time does not
29
+ * reveal how much of a token was right.
30
+ */
31
+ import { randomBytes, timingSafeEqual, createHmac } from 'node:crypto';
32
+ /** Hostnames that mean this machine. Anything else is rejected. */
33
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]', '::1']);
34
+ /** Creates the per-process secrets. */
35
+ export function createSession() {
36
+ 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
+ csrfSecret: randomBytes(32).toString('base64url'),
41
+ };
42
+ }
43
+ /** Compares two secrets without leaking their similarity through timing. */
44
+ export function safeEqual(a, b) {
45
+ const left = Buffer.from(a, 'utf8');
46
+ const right = Buffer.from(b, 'utf8');
47
+ // timingSafeEqual throws on differing lengths, which would itself be a signal,
48
+ // so pad to a fixed size first.
49
+ const size = Math.max(left.length, right.length, 1);
50
+ const paddedLeft = Buffer.alloc(size);
51
+ const paddedRight = Buffer.alloc(size);
52
+ left.copy(paddedLeft);
53
+ right.copy(paddedRight);
54
+ return timingSafeEqual(paddedLeft, paddedRight) && left.length === right.length;
55
+ }
56
+ /**
57
+ * Checks the Host header.
58
+ *
59
+ * The defence against DNS rebinding: a page at evil.example.com can point that
60
+ * name at 127.0.0.1, and the browser will happily send requests with
61
+ * `Host: evil.example.com`. Only loopback names are accepted.
62
+ */
63
+ export function isAllowedHost(host) {
64
+ if (!host)
65
+ return false;
66
+ // Strip the port. IPv6 literals keep their brackets.
67
+ const withoutPort = host.startsWith('[')
68
+ ? (host.match(/^\[[^\]]+\]/)?.[0] ?? host)
69
+ : (host.split(':')[0] ?? host);
70
+ return LOOPBACK_HOSTS.has(withoutPort.toLowerCase());
71
+ }
72
+ /**
73
+ * Checks the Origin header for state-changing requests.
74
+ *
75
+ * A missing Origin is accepted, because browsers omit it for ordinary top-level
76
+ * navigation and non-browser clients never send it. A present but foreign one is
77
+ * rejected: that is a cross-site request.
78
+ */
79
+ export function isAllowedOrigin(origin, port) {
80
+ if (!origin)
81
+ return true;
82
+ if (origin === 'null')
83
+ return false;
84
+ let parsed;
85
+ try {
86
+ parsed = new URL(origin);
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ if (parsed.protocol !== 'http:')
92
+ return false;
93
+ if (parsed.port && Number(parsed.port) !== port)
94
+ return false;
95
+ return LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase());
96
+ }
97
+ /**
98
+ * Derives a CSRF token for a form.
99
+ *
100
+ * Bound to the session secret and to the form's purpose, so a token for one
101
+ * action cannot be replayed against another.
102
+ */
103
+ export function csrfToken(secrets, purpose) {
104
+ return createHmac('sha256', secrets.csrfSecret).update(purpose).digest('base64url');
105
+ }
106
+ export function csrfValid(secrets, purpose, presented) {
107
+ return safeEqual(csrfToken(secrets, purpose), presented);
108
+ }
109
+ /**
110
+ * Runs every check in order.
111
+ *
112
+ * Order matters: the host check comes before the token check, so a rebinding
113
+ * attempt is refused before it can even try tokens.
114
+ */
115
+ export function checkRequest(options) {
116
+ const { request, secrets, port, presentedToken, writing, csrf } = options;
117
+ if (!isAllowedHost(request.headers.host))
118
+ return { ok: false, reason: 'bad-host' };
119
+ if (!presentedToken || !safeEqual(secrets.accessToken, presentedToken)) {
120
+ return { ok: false, reason: 'bad-token' };
121
+ }
122
+ if (writing) {
123
+ if (!isAllowedOrigin(request.headers.origin, port))
124
+ return { ok: false, reason: 'bad-origin' };
125
+ if (!csrf?.presented || !csrfValid(secrets, csrf.purpose, csrf.presented)) {
126
+ return { ok: false, reason: 'bad-csrf' };
127
+ }
128
+ }
129
+ return { ok: true };
130
+ }
131
+ /**
132
+ * The referrer policy.
133
+ *
134
+ * `same-origin` rather than the stricter-looking `no-referrer`, and this is not a
135
+ * concession: `no-referrer` **breaks the Origin check above**, which makes every
136
+ * form on this interface unusable.
137
+ *
138
+ * The mechanism, per the Fetch standard: when a request's referrer policy is
139
+ * `no-referrer`, the browser sets the request's origin to an opaque one, and the
140
+ * `Origin` header goes out as the literal string `null`. Since a foreign or
141
+ * sandboxed page can also produce `Origin: null`, that value has to be refused,
142
+ * so the interface would refuse its own forms. Confirmed in a real browser:
143
+ * every POST answered 404 with reason `bad-origin` until this was changed.
144
+ *
145
+ * Nothing is given up. The strict CSP already forbids this page from reaching any
146
+ * other host, and the page contains no external links, so `same-origin` has no
147
+ * third party to leak a referrer to in the first place.
148
+ *
149
+ * Do not "harden" this back to `no-referrer` without a browser to try it in.
150
+ */
151
+ export const REFERRER_POLICY = 'same-origin';
152
+ /**
153
+ * The headers every response carries.
154
+ *
155
+ * The CSP is the strict kind: nothing may be loaded from anywhere, styles are
156
+ * allowed only from the document itself, and there is no way to reach the
157
+ * network. `frame-ancestors 'none'` keeps the page out of foreign frames, which
158
+ * also rules out clickjacking.
159
+ */
160
+ export function securityHeaders() {
161
+ return {
162
+ 'Content-Security-Policy': [
163
+ "default-src 'none'",
164
+ "style-src 'self' 'unsafe-inline'",
165
+ "img-src 'self' data:",
166
+ "form-action 'self'",
167
+ "frame-ancestors 'none'",
168
+ "base-uri 'none'",
169
+ ].join('; '),
170
+ 'X-Content-Type-Options': 'nosniff',
171
+ 'X-Frame-Options': 'DENY',
172
+ 'Referrer-Policy': REFERRER_POLICY,
173
+ // The interface shows configuration, never mail. Still, nothing here should
174
+ // linger in a cache a later user could read.
175
+ 'Cache-Control': 'no-store, max-age=0',
176
+ // No browser feature this page needs is worth leaving enabled.
177
+ 'Permissions-Policy': 'geolocation=(), camera=(), microphone=(), payment=()',
178
+ };
179
+ }
180
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/web/security.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The local web interface.
3
+ *
4
+ * Built on node:http with no framework. For something that handles credentials,
5
+ * every dependency is attack surface, and the routing here fits on one screen.
6
+ *
7
+ * It serves no files from disk at all: every response is generated. That removes
8
+ * path traversal as a category rather than guarding against it.
9
+ *
10
+ * All security checks live in security.ts and run before any handler. See there
11
+ * for what each one defends against.
12
+ */
13
+ import { type IncomingMessage } from 'node:http';
14
+ import type { Credentials, CredentialStore, StoreKind } from '../credentials/store.js';
15
+ /**
16
+ * The port tried first.
17
+ *
18
+ * A fixed default rather than a random one, because the interface runs for as
19
+ * long as the server does and is meant to be opened again and again. A port that
20
+ * moved on every start would make that needlessly awkward.
21
+ */
22
+ export declare const DEFAULT_WEB_PORT = 7345;
23
+ /**
24
+ * How many consecutive ports to try.
25
+ *
26
+ * Several clients may each start their own server, and the second one must not
27
+ * fail over a port the first one took. Failing outright would be worse than
28
+ * moving up by one.
29
+ */
30
+ export declare const PORT_ATTEMPTS = 10;
31
+ export interface WebInterfaceOptions {
32
+ /** 0 lets the operating system pick a free port, which is the default. */
33
+ port?: number;
34
+ /**
35
+ * Called when the user signs in. Returns an error message to show, or nothing
36
+ * on success. The master password is only set for the encrypted-file store.
37
+ */
38
+ onSignIn: (credentials: Credentials, store: StoreKind, masterPassword?: string) => Promise<string | undefined>;
39
+ /** Called when the user signs out. */
40
+ onSignOut: () => Promise<void>;
41
+ /**
42
+ * Opens an existing encrypted credentials file. Returns an error message to
43
+ * show, or nothing on success.
44
+ *
45
+ * Only reached when getStatus reports locked, so an interface wired without
46
+ * this cannot get here.
47
+ */
48
+ onUnlock?: (masterPassword: string) => Promise<string | undefined>;
49
+ /** Throws the encrypted file away, for a forgotten master password. */
50
+ onDiscardEncrypted?: () => Promise<void>;
51
+ /** Supplies what the status page shows. */
52
+ getStatus: () => Promise<StatusSnapshot>;
53
+ /**
54
+ * Changes the Bridge ports. Returns an error message to show, or nothing.
55
+ *
56
+ * Absent when the ports came from the environment, which is what makes the
57
+ * form disappear rather than fail.
58
+ */
59
+ onPorts?: (ports: {
60
+ imapPort: number;
61
+ smtpPort: number;
62
+ }) => Promise<string | undefined>;
63
+ /** Tries the Bridge. Returns a message either way: this one reports success. */
64
+ onTest?: () => Promise<{
65
+ ok: boolean;
66
+ message: string;
67
+ }>;
68
+ /** Lists the mailboxes, for the page that shows them on request. */
69
+ getMailboxes?: () => Promise<Array<{
70
+ path: string;
71
+ kind: 'system' | 'folder' | 'label';
72
+ selectable: boolean;
73
+ }>>;
74
+ /** What is running and what ran, for the activity section. */
75
+ getActivity?: () => {
76
+ running: Array<{
77
+ tool: string;
78
+ forMs: number;
79
+ }>;
80
+ recent: Array<{
81
+ tool: string;
82
+ at: number;
83
+ durationMs: number;
84
+ outcome: 'ok' | 'failed';
85
+ }>;
86
+ };
87
+ /**
88
+ * Whether the "not affiliated with Proton" notice was sent away already.
89
+ *
90
+ * Absent means it is shown without a way to dismiss it, which is the safe
91
+ * direction: a legal statement should not vanish because a caller left a
92
+ * handler out.
93
+ */
94
+ noticeDismissed?: () => boolean;
95
+ /** Remembers a dismissal. Without it the notice has no button. */
96
+ onDismissNotice?: () => Promise<void>;
97
+ /** Where notices go. stderr, never stdout, which carries the MCP protocol. */
98
+ notify?: (message: string) => void;
99
+ }
100
+ /** Everything the status page needs, gathered by the caller. */
101
+ export interface StatusSnapshot {
102
+ connected: boolean;
103
+ /**
104
+ * True when credentials are stored but need a master password first. Leads
105
+ * to the unlock page instead of the full sign-in form.
106
+ */
107
+ locked?: boolean;
108
+ /** Where the encrypted file is, shown on the unlock page. */
109
+ encryptedPath?: string;
110
+ address?: string;
111
+ storeKind?: StoreKind;
112
+ mailboxCount?: number;
113
+ messageCount?: number;
114
+ unseenCount?: number;
115
+ certificate?: {
116
+ fingerprint: string;
117
+ subject: string;
118
+ firstSeen: string;
119
+ };
120
+ bridgeHost: string;
121
+ bridgeImapPort: number;
122
+ /**
123
+ * Required, not optional. It used to fall back to 0, which is not a port:
124
+ * the advanced block would prefill a value the same form then refused.
125
+ */
126
+ bridgeSmtpPort: number;
127
+ readOnly?: boolean;
128
+ uptimeMs?: number;
129
+ }
130
+ interface ParsedBody {
131
+ fields: Record<string, string>;
132
+ tooLarge: boolean;
133
+ }
134
+ /**
135
+ * Reads a form body, refusing anything oversized.
136
+ *
137
+ * The limit is enforced while reading rather than after, so an oversized body is
138
+ * never held in memory in full.
139
+ */
140
+ export declare function readBody(request: IncomingMessage, maxBytes?: number): Promise<ParsedBody>;
141
+ export declare class WebInterface {
142
+ #private;
143
+ constructor(options: WebInterfaceOptions);
144
+ /** The URL the user has to open, including the access token. */
145
+ get url(): string;
146
+ get port(): number;
147
+ get running(): boolean;
148
+ start(): Promise<string>;
149
+ stop(): Promise<void>;
150
+ /** Invalidates the current link and issues a new one. */
151
+ rotateToken(): string;
152
+ }
153
+ export type { CredentialStore };