@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,590 @@
1
+ /**
2
+ * The frame every page of the local interface sits in.
3
+ *
4
+ * Split out of pages.ts when the single status page became a set of sections:
5
+ * the styles, the navigation and the outer document are the same everywhere, and
6
+ * they were the larger half of that file.
7
+ *
8
+ * **There is no JavaScript here, and none anywhere in this interface.** The
9
+ * Content-Security-Policy forbids it, and that is not a limitation worked
10
+ * around: a page that cannot run script cannot be made to do anything by markup
11
+ * that finds its way in. So navigation is links, switches are form controls, and
12
+ * anything that looks interactive is CSS. See security.ts for the rest of the
13
+ * policy.
14
+ *
15
+ * The look follows Proton's own interfaces closely enough to feel related and
16
+ * uses none of their assets. No logo, no wordmark, no icon of theirs: those are
17
+ * trademarks, and every page says in as many words that this is not their
18
+ * project.
19
+ */
20
+ /** Escapes text for use in HTML. Applied to everything, without exception. */
21
+ export function escapeHtml(value) {
22
+ return value
23
+ .replace(/&/g, '&')
24
+ .replace(/</g, '&lt;')
25
+ .replace(/>/g, '&gt;')
26
+ .replace(/"/g, '&quot;')
27
+ .replace(/'/g, '&#39;');
28
+ }
29
+ /*
30
+ * Icons are inline SVG rather than files.
31
+ *
32
+ * Not a style choice: the CSP allows no external resource of any kind, and a
33
+ * served file would be a path this server has to resolve. Inline markup is
34
+ * neither. They are drawn from a single 24-unit grid with one stroke width, so
35
+ * they sit level with the text beside them.
36
+ */
37
+ const ICONS = {
38
+ overview: '<path d="M4 5h6v6H4zM14 5h6v6h-6zM4 15h6v4H4zM14 15h6v4h-6z"/>',
39
+ mailboxes: '<path d="M3 7l2-2h5l2 2h9v12H3z"/>',
40
+ bridge: '<path d="M7 8V5m10 3V5M5 8h14v4a7 7 0 0 1-14 0z"/><path d="M12 19v2"/>',
41
+ credentials: '<circle cx="9" cy="12" r="4"/><path d="M13 12h8M18 12v4"/>',
42
+ activity: '<path d="M3 12h4l3 7 4-14 3 7h4"/>',
43
+ };
44
+ function icon(name) {
45
+ return (`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" ` +
46
+ `stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name]}</svg>`);
47
+ }
48
+ /** Which entries exist. Their availability is decided by the caller. */
49
+ function navigation() {
50
+ return [
51
+ {
52
+ entries: [{ section: 'overview', path: '/', label: 'Overview', icon: icon('overview') }],
53
+ },
54
+ {
55
+ heading: 'Proton Mail',
56
+ entries: [
57
+ {
58
+ section: 'mailboxes',
59
+ path: '/mailboxes',
60
+ label: 'Folders and labels',
61
+ icon: icon('mailboxes'),
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ heading: 'Setup',
67
+ entries: [
68
+ { section: 'bridge', path: '/bridge', label: 'Bridge', icon: icon('bridge') },
69
+ {
70
+ section: 'credentials',
71
+ path: '/credentials',
72
+ label: 'Credentials',
73
+ icon: icon('credentials'),
74
+ },
75
+ { section: 'activity', path: '/activity', label: 'Activity', icon: icon('activity') },
76
+ ],
77
+ },
78
+ ];
79
+ }
80
+ /**
81
+ * The stylesheet.
82
+ *
83
+ * Both colour schemes are defined rather than one: Proton's own interfaces
84
+ * follow the system setting, and a configuration page that glares white at
85
+ * somebody working in the dark is the kind of detail that makes a tool feel
86
+ * unfinished. `prefers-color-scheme` needs no script and no stored preference.
87
+ */
88
+ const STYLES = `
89
+ :root {
90
+ --violet: #6d4aff;
91
+ --violet-strong: #5b35f0;
92
+ --violet-soft: #efeaff;
93
+ --rail: #1b1340;
94
+ --rail-text: #d8d2f5;
95
+ --rail-active: rgba(255,255,255,0.13);
96
+ --text: #22143f;
97
+ --muted: #6b6483;
98
+ --line: #e3e0ee;
99
+ --surface: #ffffff;
100
+ --page: #f5f4fa;
101
+ --code: #efedf6;
102
+ --warn-bg: #fff8e6;
103
+ --warn-line: #e8c56b;
104
+ --warn-text: #7a5a12;
105
+ --bad-bg: #fdeaea;
106
+ --bad-line: #e08585;
107
+ --bad-text: #8f2d2d;
108
+ --good-bg: #eaf7ef;
109
+ --good-line: #7fc899;
110
+ --good-text: #2f6b46;
111
+ --shadow: 0 1px 2px rgba(27,19,64,0.06), 0 1px 3px rgba(27,19,64,0.04);
112
+ }
113
+ @media (prefers-color-scheme: dark) {
114
+ :root {
115
+ --violet: #8a6dff;
116
+ --violet-strong: #9b83ff;
117
+ --violet-soft: #2a2145;
118
+ --rail: #151122;
119
+ --rail-text: #b9b2d6;
120
+ --rail-active: rgba(255,255,255,0.09);
121
+ --text: #e7e3f4;
122
+ --muted: #a09ab8;
123
+ --line: #322d45;
124
+ --surface: #1e1b2b;
125
+ --page: #14121d;
126
+ --code: #2a2639;
127
+ --warn-bg: #2e2717;
128
+ --warn-line: #7a6224;
129
+ --warn-text: #e6c878;
130
+ --bad-bg: #331c1c;
131
+ --bad-line: #7d3b3b;
132
+ --bad-text: #f0a3a3;
133
+ --good-bg: #17291f;
134
+ --good-line: #3c6b4d;
135
+ --good-text: #8fd3aa;
136
+ --shadow: 0 1px 2px rgba(0,0,0,0.3);
137
+ }
138
+ }
139
+
140
+ * { box-sizing: border-box; }
141
+ html { color-scheme: light dark; }
142
+ body {
143
+ margin: 0;
144
+ min-height: 100vh;
145
+ background: var(--page);
146
+ color: var(--text);
147
+ font: 15px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
148
+ }
149
+
150
+ /* The frame: a fixed rail beside a column that scrolls on its own. */
151
+ .frame { display: flex; min-height: 100vh; }
152
+
153
+ .rail {
154
+ flex: 0 0 244px;
155
+ background: var(--rail);
156
+ color: var(--rail-text);
157
+ padding: 20px 14px;
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 6px;
161
+ }
162
+ .brand { padding: 4px 10px 18px; }
163
+ .brand b { display: block; color: #fff; font-size: 17px; font-weight: 600; letter-spacing: 0.01em; }
164
+ .brand .unofficial {
165
+ display: inline-block;
166
+ margin-top: 7px;
167
+ padding: 2px 8px;
168
+ border: 1px solid rgba(255,255,255,0.35);
169
+ border-radius: 999px;
170
+ font-size: 11px;
171
+ text-transform: uppercase;
172
+ letter-spacing: 0.07em;
173
+ }
174
+ .rail .group { margin-top: 16px; }
175
+ .rail .group-name {
176
+ margin: 0 0 6px;
177
+ padding: 0 10px;
178
+ font-size: 11px;
179
+ font-weight: 600;
180
+ text-transform: uppercase;
181
+ letter-spacing: 0.08em;
182
+ color: rgba(255,255,255,0.42);
183
+ }
184
+ .rail a {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 10px;
188
+ padding: 8px 10px;
189
+ border-radius: 8px;
190
+ color: var(--rail-text);
191
+ text-decoration: none;
192
+ font-size: 14px;
193
+ }
194
+ .rail a:hover { background: rgba(255,255,255,0.07); color: #fff; }
195
+ .rail a[aria-current] { background: var(--rail-active); color: #fff; font-weight: 600; }
196
+ .rail svg { width: 18px; height: 18px; flex: 0 0 18px; opacity: 0.85; }
197
+ .rail a[aria-current] svg { opacity: 1; }
198
+ .rail .foot {
199
+ margin-top: auto;
200
+ padding: 14px 10px 0;
201
+ font-size: 12px;
202
+ line-height: 1.5;
203
+ color: rgba(255,255,255,0.42);
204
+ }
205
+
206
+ .content { flex: 1 1 auto; min-width: 0; }
207
+ main { max-width: 780px; margin: 0 auto; padding: 36px 32px 56px; }
208
+
209
+ h1 { font-size: 25px; line-height: 1.25; margin: 0 0 6px; letter-spacing: -0.01em; }
210
+ h2 { font-size: 15px; margin: 34px 0 10px; letter-spacing: 0.01em; }
211
+ h3 { font-size: 15px; margin: 0; }
212
+ p { margin: 8px 0; }
213
+ .lead { color: var(--muted); margin: 0 0 22px; }
214
+
215
+ .card {
216
+ background: var(--surface);
217
+ border: 1px solid var(--line);
218
+ border-radius: 12px;
219
+ padding: 20px 22px;
220
+ margin: 12px 0;
221
+ box-shadow: var(--shadow);
222
+ }
223
+ .card > :first-child { margin-top: 0; }
224
+ .card > :last-child { margin-bottom: 0; }
225
+
226
+ .disclaimer {
227
+ display: flex;
228
+ align-items: flex-start;
229
+ gap: 12px;
230
+ background: var(--warn-bg);
231
+ border: 1px solid var(--warn-line);
232
+ color: var(--warn-text);
233
+ border-radius: 12px;
234
+ padding: 12px 16px;
235
+ margin: 0 0 22px;
236
+ font-size: 13px;
237
+ }
238
+ .disclaimer p { margin: 0; flex: 1 1 auto; }
239
+ .disclaimer strong { color: inherit; }
240
+ .disclaimer form { flex: 0 0 auto; }
241
+ button.dismiss {
242
+ margin: -2px -4px 0 0;
243
+ padding: 0;
244
+ width: 24px;
245
+ height: 24px;
246
+ background: none;
247
+ border: 0;
248
+ border-radius: 6px;
249
+ color: inherit;
250
+ opacity: 0.65;
251
+ font-size: 19px;
252
+ line-height: 1;
253
+ }
254
+ button.dismiss:hover { background: rgba(0,0,0,0.07); opacity: 1; }
255
+
256
+ label { display: block; margin: 16px 0 4px; font-weight: 600; font-size: 13px; }
257
+ input[type=text], input[type=password] {
258
+ width: 100%;
259
+ padding: 10px 12px;
260
+ border: 1px solid var(--line);
261
+ border-radius: 8px;
262
+ background: var(--surface);
263
+ color: var(--text);
264
+ font: inherit;
265
+ }
266
+ input:focus-visible {
267
+ outline: 2px solid var(--violet);
268
+ outline-offset: 1px;
269
+ border-color: var(--violet);
270
+ }
271
+ button {
272
+ margin-top: 18px;
273
+ background: var(--violet);
274
+ color: #fff;
275
+ border: 0;
276
+ border-radius: 8px;
277
+ padding: 10px 18px;
278
+ font: inherit;
279
+ font-weight: 600;
280
+ cursor: pointer;
281
+ }
282
+ button:hover { background: var(--violet-strong); }
283
+ button:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
284
+ /*
285
+ * Actions that destroy something do not get the colour of the action you are
286
+ * meant to take. Deleting the credentials file and signing out both belong
287
+ * here: they are the right answer sometimes, never the obvious one.
288
+ */
289
+ button.danger {
290
+ background: var(--surface);
291
+ color: var(--bad-text);
292
+ border: 1px solid var(--bad-line);
293
+ }
294
+ button.danger:hover { background: var(--bad-bg); }
295
+
296
+ .hint { color: var(--muted); font-size: 13px; margin-top: 6px; }
297
+
298
+ /* The disclosure element, used where a setting is real but rarely needed. */
299
+ details.advanced summary {
300
+ cursor: pointer;
301
+ font-weight: 600;
302
+ font-size: 14px;
303
+ list-style-position: outside;
304
+ }
305
+ details.advanced summary::marker { color: var(--muted); }
306
+ details.advanced[open] summary { margin-bottom: 8px; }
307
+ details.advanced input:disabled {
308
+ background: var(--code);
309
+ color: var(--muted);
310
+ cursor: not-allowed;
311
+ }
312
+ /*
313
+ * The storage options.
314
+ *
315
+ * Each is a label wrapping a real radio, so the whole card is the click
316
+ * target while keyboard and assistive technology get the ordinary control.
317
+ * The selected state is drawn from :checked rather than from a class the
318
+ * server decided, which is what lets the choice change without a round trip.
319
+ */
320
+ .option {
321
+ display: flex;
322
+ gap: 12px;
323
+ align-items: flex-start;
324
+ border: 1px solid var(--line);
325
+ border-radius: 10px;
326
+ padding: 14px 16px;
327
+ margin: 10px 0;
328
+ background: var(--surface);
329
+ cursor: pointer;
330
+ }
331
+ .option:hover { border-color: var(--muted); }
332
+ .option.unavailable { opacity: 0.62; cursor: not-allowed; }
333
+ .option input[type=radio] { margin: 5px 0 0; width: 17px; height: 17px; accent-color: var(--violet); }
334
+ .option-body { flex: 1 1 auto; min-width: 0; }
335
+ .option:has(input:checked) { border-color: var(--violet); box-shadow: 0 0 0 1px var(--violet); }
336
+ .option:has(input:focus-visible) { outline: 2px solid var(--violet); outline-offset: 2px; }
337
+ .option header { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
338
+ .option h3 { font-weight: 600; }
339
+
340
+ /*
341
+ * The two blocks that follow the selection.
342
+ *
343
+ * :has() on the form rather than a sibling selector, so the markup can stay in
344
+ * the order a person reads it instead of the order CSS needs. Supported by
345
+ * every current browser; where it is not, the @supports rule below shows both
346
+ * blocks rather than hiding a field somebody needs to fill in. An interface
347
+ * that silently omits the master password field would be unusable in a way
348
+ * nobody could diagnose.
349
+ */
350
+ .master-fields, .warning-layer { display: none; }
351
+ form.sign-in:has(#store-encrypted-file:checked) .master-fields { display: block; }
352
+ form.sign-in:has(#store-plain-file:checked):has(#warning-seen:not(:checked)) .warning-layer {
353
+ display: flex;
354
+ }
355
+ @supports not selector(:has(*)) {
356
+ .master-fields { display: block; }
357
+ }
358
+
359
+ /* Visually gone, still a control: the checkbox the warning's button toggles. */
360
+ .offscreen {
361
+ position: absolute;
362
+ width: 1px; height: 1px;
363
+ margin: -1px;
364
+ padding: 0;
365
+ overflow: hidden;
366
+ clip-path: inset(50%);
367
+ white-space: nowrap;
368
+ }
369
+
370
+ .warning-layer {
371
+ position: fixed;
372
+ inset: 0;
373
+ z-index: 10;
374
+ align-items: center;
375
+ justify-content: center;
376
+ background: rgba(20,15,45,0.55);
377
+ }
378
+ /*
379
+ * Clicking beside the box closes it too, which is what a dialog does. An
380
+ * empty label rather than a handler, since there is no script here; it sits
381
+ * beside the box rather than around it, because a label closes on any click
382
+ * inside itself and the box has to stay usable.
383
+ */
384
+ /* margin: 0 undoes the general label rule, which would otherwise inset it by
385
+ * 16px at the top and leave a strip the click does not reach. */
386
+ .warning-backdrop { position: absolute; inset: 0; margin: 0; cursor: default; }
387
+ .warning-box {
388
+ position: relative;
389
+ background: var(--surface);
390
+ border: 1px solid var(--warn-line);
391
+ border-top: 4px solid var(--warn-line);
392
+ border-radius: 12px;
393
+ padding: 22px 24px;
394
+ /* Margin rather than padding on the layer, so the backdrop reaches the
395
+ * very edge and a click anywhere outside the box closes it. */
396
+ margin: 20px;
397
+ max-width: 460px;
398
+ max-height: calc(100vh - 40px);
399
+ overflow-y: auto;
400
+ box-shadow: 0 12px 40px rgba(20,15,45,0.35);
401
+ }
402
+ .warning-box h3 { margin: 0 0 10px; font-size: 17px; }
403
+ .warning-box p { font-size: 14px; }
404
+ .warning-close {
405
+ display: inline-block;
406
+ margin: 18px 0 0;
407
+ padding: 10px 18px;
408
+ border-radius: 8px;
409
+ background: var(--violet);
410
+ color: #fff;
411
+ font-weight: 600;
412
+ font-size: 14px;
413
+ cursor: pointer;
414
+ }
415
+ .warning-close:hover { background: var(--violet-strong); }
416
+
417
+ .tag {
418
+ font-size: 11px;
419
+ padding: 2px 8px;
420
+ border-radius: 999px;
421
+ border: 1px solid var(--line);
422
+ color: var(--muted);
423
+ white-space: nowrap;
424
+ }
425
+ .tag.exposure-none, .tag.exposure-os-protected {
426
+ background: var(--good-bg); border-color: var(--good-line); color: var(--good-text);
427
+ }
428
+ .tag.exposure-encrypted {
429
+ background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn-text);
430
+ }
431
+ .tag.exposure-plaintext {
432
+ background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad-text);
433
+ }
434
+ .tag.suggested { background: var(--violet-soft); border-color: var(--violet); color: var(--violet-strong); }
435
+ .tag.chosen { background: var(--violet); border-color: var(--violet); color: #fff; }
436
+
437
+ .pro, .con, .who { font-size: 13px; margin: 6px 0; }
438
+ .pro::before { content: "Good: "; font-weight: 600; color: var(--good-text); }
439
+ .con::before { content: "Cost: "; font-weight: 600; color: var(--bad-text); }
440
+ .who::before { content: "Best for: "; font-weight: 600; color: var(--muted); }
441
+ .blocked { font-size: 13px; margin: 6px 0; color: var(--bad-text); }
442
+ .blocked::before { content: "Not available here: "; font-weight: 600; }
443
+
444
+ table { border-collapse: collapse; width: 100%; font-size: 14px; }
445
+ th, td { text-align: left; padding: 9px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
446
+ tr:last-child th, tr:last-child td { border-bottom: 0; }
447
+ th { color: var(--muted); font-weight: 500; width: 40%; }
448
+ code {
449
+ background: var(--code);
450
+ padding: 1px 5px;
451
+ border-radius: 4px;
452
+ font-size: 12.5px;
453
+ word-break: break-all;
454
+ }
455
+
456
+ /* A value that reads as a state rather than as a number. */
457
+ .state { display: inline-flex; align-items: center; gap: 7px; }
458
+ .state::before {
459
+ content: "";
460
+ width: 8px; height: 8px;
461
+ border-radius: 50%;
462
+ background: var(--muted);
463
+ }
464
+ .state.good::before { background: #33a06f; }
465
+ .state.bad::before { background: #d05353; }
466
+
467
+ .error, .notice {
468
+ border-radius: 12px;
469
+ padding: 13px 16px;
470
+ margin: 0 0 18px;
471
+ font-size: 14px;
472
+ }
473
+ .error { background: var(--bad-bg); border: 1px solid var(--bad-line); color: var(--bad-text); }
474
+ .notice { background: var(--good-bg); border: 1px solid var(--good-line); color: var(--good-text); }
475
+
476
+ a { color: var(--violet-strong); }
477
+ .content a:focus-visible, .rail a:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
478
+
479
+ /*
480
+ * Narrow windows put the rail on top as a scrolling row. A configuration page
481
+ * is opened on a phone more often than its author expects, usually because the
482
+ * link was sent from the machine it runs on.
483
+ */
484
+ @media (max-width: 860px) {
485
+ .frame { display: block; }
486
+ .rail { flex: none; flex-direction: row; align-items: center; gap: 4px; overflow-x: auto; padding: 10px 12px; }
487
+ .brand { padding: 0 12px 0 4px; flex: 0 0 auto; }
488
+ .brand .unofficial { margin-top: 2px; }
489
+ .rail .group { margin: 0; display: flex; align-items: center; gap: 4px; }
490
+ .rail .group-name { display: none; }
491
+ .rail a { white-space: nowrap; }
492
+ .rail .foot { display: none; }
493
+ main { padding: 24px 18px 40px; }
494
+ }
495
+ `;
496
+ /** The statement that this is not Proton's software. */
497
+ const DISCLAIMER = 'This is <strong>proton-mcp</strong>, an unofficial open source tool. It is not made by, ' +
498
+ 'affiliated with or endorsed by Proton AG. "Proton" and "Proton Mail" are their trademarks. ' +
499
+ 'This interface only talks to the Proton Mail Bridge running on your own machine.';
500
+ function disclaimerBanner(state, token) {
501
+ if (state === 'dismissed')
502
+ return '';
503
+ if (state === 'plain' || token === undefined) {
504
+ return `<div class="disclaimer"><p>${DISCLAIMER}</p></div>`;
505
+ }
506
+ return `<div class="disclaimer">
507
+ <p>${DISCLAIMER}</p>
508
+ <form method="post" action="/dismiss-notice?token=${escapeHtml(token)}">
509
+ <input type="hidden" name="csrf" value="${escapeHtml(state.csrf)}">
510
+ <input type="hidden" name="from" value="${escapeHtml(state.from)}">
511
+ <button type="submit" class="dismiss" title="Do not show this again"
512
+ aria-label="Do not show this again">&times;</button>
513
+ </form>
514
+ </div>`;
515
+ }
516
+ function rail(current, token) {
517
+ const groups = navigation()
518
+ .map((group) => {
519
+ const links = group.entries
520
+ .map((entry) => {
521
+ const href = `${entry.path}?token=${escapeHtml(token)}`;
522
+ const mark = entry.section === current ? ' aria-current="page"' : '';
523
+ return `<a href="${href}"${mark}>${entry.icon}<span>${escapeHtml(entry.label)}</span></a>`;
524
+ })
525
+ .join('\n ');
526
+ // The group heading is a plain element and the name goes on the nav as a
527
+ // label. A real heading here would put "Setup" above the page's own <h1>
528
+ // in the outline, and anything reading the page aloud would announce the
529
+ // furniture before the room.
530
+ const heading = group.heading ? `<p class="group-name">${escapeHtml(group.heading)}</p>` : '';
531
+ const label = group.heading ? ` aria-label="${escapeHtml(group.heading)}"` : '';
532
+ return `<nav class="group"${label}>${heading}\n ${links}\n </nav>`;
533
+ })
534
+ .join('\n ');
535
+ return `<div class="rail">
536
+ <div class="brand"><b>proton-mcp</b><span class="unofficial">unofficial</span></div>
537
+ ${groups}
538
+ <p class="foot">Running on 127.0.0.1 only.<br>Not reachable from anywhere else.</p>
539
+ </div>`;
540
+ }
541
+ export function layout(options) {
542
+ const { title, body, section, token, disclaimer = 'plain' } = options;
543
+ const navigable = section !== 'none' && token !== undefined;
544
+ return `<!DOCTYPE html>
545
+ <html lang="en">
546
+ <head>
547
+ <meta charset="utf-8">
548
+ <meta name="viewport" content="width=device-width, initial-scale=1">
549
+ <meta name="robots" content="noindex, nofollow">
550
+ <title>${escapeHtml(title)} - proton-mcp</title>
551
+ <style>${STYLES}</style>
552
+ </head>
553
+ <body>
554
+ <div class="frame">
555
+ ${navigable ? rail(section, token) : ''}
556
+ <div class="content">
557
+ <main>
558
+ ${disclaimerBanner(disclaimer, token)}
559
+ ${body}
560
+ </main>
561
+ </div>
562
+ </div>
563
+ </body>
564
+ </html>
565
+ `;
566
+ }
567
+ /** Renders a duration the way a person would say it. */
568
+ export function formatDuration(ms) {
569
+ if (ms < 1000)
570
+ return `${Math.max(0, Math.round(ms))} ms`;
571
+ const seconds = Math.floor(ms / 1000);
572
+ if (seconds < 60)
573
+ return `${seconds} s`;
574
+ const minutes = Math.floor(seconds / 60);
575
+ if (minutes < 60)
576
+ return `${minutes} min ${seconds % 60} s`;
577
+ const hours = Math.floor(minutes / 60);
578
+ if (hours < 24)
579
+ return `${hours} h ${minutes % 60} min`;
580
+ return `${Math.floor(hours / 24)} d ${hours % 24} h`;
581
+ }
582
+ /** An error banner, or nothing. */
583
+ export function errorBanner(message) {
584
+ return message ? `<div class="error">${escapeHtml(message)}</div>` : '';
585
+ }
586
+ /** A success banner, or nothing. */
587
+ export function noticeBanner(message) {
588
+ return message ? `<div class="notice">${escapeHtml(message)}</div>` : '';
589
+ }
590
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/web/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The pages that come before anybody is signed in.
3
+ *
4
+ * Rendered as strings rather than with a template engine: it keeps the
5
+ * dependency count at zero for something that handles credentials, and there are
6
+ * only a handful of pages.
7
+ *
8
+ * Everything that could come from outside goes through escapeHtml. The CSP would
9
+ * already stop injected script from doing anything, but not from making a mess of
10
+ * the page, and relying on a single layer is how holes happen.
11
+ *
12
+ * These three carry no navigation. There is nothing to navigate to yet, and a
13
+ * rail full of links that all lead back here would suggest otherwise. The
14
+ * sections that appear afterwards are in sections.ts, the frame they share in
15
+ * layout.ts.
16
+ */
17
+ import { type DisclaimerState } from './layout.js';
18
+ import { type StoreKind } from '../credentials/store.js';
19
+ import type { StoreAvailability } from '../credentials/availability.js';
20
+ export { escapeHtml, formatDuration } from './layout.js';
21
+ export interface LoginPageData {
22
+ token: string;
23
+ csrf: string;
24
+ /** See sections.ts: required so that it cannot be lost by omission. */
25
+ disclaimer: DisclaimerState;
26
+ /** Where the Bridge is expected, prefilled into the advanced block. */
27
+ bridgeHost: string;
28
+ bridgeImapPort: number;
29
+ bridgeSmtpPort: number;
30
+ /** True when the ports came from the environment and are not ours to change. */
31
+ portsLocked: boolean;
32
+ /** Opens the advanced block, for when the ports are what went wrong. */
33
+ showAdvanced?: boolean;
34
+ stores: StoreAvailability[];
35
+ suggested: StoreKind;
36
+ suggestionReason: string;
37
+ /** What the user picked, if they picked anything. Falls back to the suggestion. */
38
+ chosen?: StoreKind;
39
+ /** Shown when a previous attempt failed. */
40
+ error?: string;
41
+ /** Prefilled address, so a failed attempt does not lose it. */
42
+ address?: string;
43
+ }
44
+ /**
45
+ * The sign-in form.
46
+ *
47
+ * One form for everything: the address, the password and where the password
48
+ * goes. Nothing is submitted until Connect is pressed, so choosing among the
49
+ * four options costs no round trip and the page does not move under the
50
+ * pointer.
51
+ *
52
+ * Two things appear and disappear with the selection, both in CSS: the master
53
+ * password fields for the encrypted file, and the warning for the plain file.
54
+ * See the stylesheet for how, and for what happens in a browser too old to do
55
+ * it.
56
+ */
57
+ export declare function loginPage(data: LoginPageData): string;
58
+ export interface UnlockPageData {
59
+ token: string;
60
+ csrf: string;
61
+ disclaimer: DisclaimerState;
62
+ /** CSRF token for throwing the file away, bound to that action alone. */
63
+ csrfDiscard: string;
64
+ path: string;
65
+ error?: string;
66
+ }
67
+ /**
68
+ * Asked for when an encrypted credentials file exists but is not open yet.
69
+ *
70
+ * The counterpart to the sign-in form, and the reason the encrypted option is
71
+ * usable at all: after a restart the Bridge password is still on disk, so the
72
+ * only thing missing is the one password that decrypts it.
73
+ *
74
+ * It also offers the way out. Someone who forgot their master password cannot
75
+ * be left on a page that only accepts the thing they do not have.
76
+ */
77
+ export declare function unlockPage(data: UnlockPageData): string;
78
+ /** Shown when a request is rejected. Deliberately says little. */
79
+ export declare function rejectionPage(): string;