fluxmail 0.16.0 → 0.17.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.
Files changed (79) hide show
  1. package/dist/auth.d.ts +22 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +29 -3
  4. package/dist/auth.js.map +1 -1
  5. package/dist/authorization.d.ts +2 -0
  6. package/dist/authorization.d.ts.map +1 -1
  7. package/dist/authorization.js +9 -4
  8. package/dist/authorization.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +93 -5
  11. package/dist/cli.js.map +1 -1
  12. package/dist/cliMail.d.ts.map +1 -1
  13. package/dist/cliMail.js +21 -1
  14. package/dist/cliMail.js.map +1 -1
  15. package/dist/deploymentConfig.d.ts +1 -1
  16. package/dist/deploymentConfig.d.ts.map +1 -1
  17. package/dist/deploymentConfig.js +1 -1
  18. package/dist/deploymentConfig.js.map +1 -1
  19. package/dist/http/app.d.ts +3 -0
  20. package/dist/http/app.d.ts.map +1 -1
  21. package/dist/http/app.js +37 -22
  22. package/dist/http/app.js.map +1 -1
  23. package/dist/http/identity.d.ts +20 -0
  24. package/dist/http/identity.d.ts.map +1 -1
  25. package/dist/http/identity.js +146 -0
  26. package/dist/http/identity.js.map +1 -1
  27. package/dist/http/oauth.d.ts +28 -0
  28. package/dist/http/oauth.d.ts.map +1 -0
  29. package/dist/http/oauth.js +552 -0
  30. package/dist/http/oauth.js.map +1 -0
  31. package/dist/http/oauthPages.d.ts +36 -0
  32. package/dist/http/oauthPages.d.ts.map +1 -0
  33. package/dist/http/oauthPages.js +213 -0
  34. package/dist/http/oauthPages.js.map +1 -0
  35. package/dist/http/rest.d.ts.map +1 -1
  36. package/dist/http/rest.js +13 -7
  37. package/dist/http/rest.js.map +1 -1
  38. package/dist/mcp/attachmentContent.d.ts +1 -1
  39. package/dist/mcp/attachmentContent.d.ts.map +1 -1
  40. package/dist/mcp/buildServer.d.ts +1 -1
  41. package/dist/mcp/buildServer.d.ts.map +1 -1
  42. package/dist/mcp/buildServer.js +77 -46
  43. package/dist/mcp/buildServer.js.map +1 -1
  44. package/dist/mcp/clientTelemetry.d.ts +19 -0
  45. package/dist/mcp/clientTelemetry.d.ts.map +1 -0
  46. package/dist/mcp/clientTelemetry.js +152 -0
  47. package/dist/mcp/clientTelemetry.js.map +1 -0
  48. package/dist/mcp/outputSchemas.d.ts +6 -6
  49. package/dist/oauth/clientMetadata.d.ts +39 -0
  50. package/dist/oauth/clientMetadata.d.ts.map +1 -0
  51. package/dist/oauth/clientMetadata.js +234 -0
  52. package/dist/oauth/clientMetadata.js.map +1 -0
  53. package/dist/oauth/redirectUris.d.ts +13 -0
  54. package/dist/oauth/redirectUris.d.ts.map +1 -0
  55. package/dist/oauth/redirectUris.js +78 -0
  56. package/dist/oauth/redirectUris.js.map +1 -0
  57. package/dist/permissions.d.ts +4 -0
  58. package/dist/permissions.d.ts.map +1 -1
  59. package/dist/permissions.js +10 -0
  60. package/dist/permissions.js.map +1 -1
  61. package/dist/service/emailService.d.ts.map +1 -1
  62. package/dist/service/emailService.js +4 -1
  63. package/dist/service/emailService.js.map +1 -1
  64. package/dist/service/publicErrors.d.ts +4 -0
  65. package/dist/service/publicErrors.d.ts.map +1 -1
  66. package/dist/service/publicErrors.js +16 -1
  67. package/dist/service/publicErrors.js.map +1 -1
  68. package/dist/storage/db.d.ts +824 -2
  69. package/dist/storage/db.d.ts.map +1 -1
  70. package/dist/storage/db.js +142 -1
  71. package/dist/storage/db.js.map +1 -1
  72. package/dist/storage/members.d.ts.map +1 -1
  73. package/dist/storage/members.js +2 -0
  74. package/dist/storage/members.js.map +1 -1
  75. package/dist/storage/oauth.d.ts +178 -0
  76. package/dist/storage/oauth.d.ts.map +1 -0
  77. package/dist/storage/oauth.js +556 -0
  78. package/dist/storage/oauth.js.map +1 -0
  79. package/package.json +9 -6
@@ -0,0 +1,213 @@
1
+ import { isLocalRedirectUri } from '../oauth/redirectUris.js';
2
+ export const OAUTH_PAGE_HEADERS = {
3
+ 'cache-control': 'no-store',
4
+ 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; img-src 'none'; base-uri 'none'; frame-ancestors 'none'",
5
+ 'x-content-type-options': 'nosniff',
6
+ 'x-frame-options': 'DENY',
7
+ // same-origin keeps the Origin header on the consent form and hides this page from the client.
8
+ 'referrer-policy': 'same-origin',
9
+ };
10
+ const icon = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
11
+ // Lucide icon paths, drawn inline because the page CSP blocks external images.
12
+ const SCOPE_LABELS = {
13
+ 'mail.read': {
14
+ label: 'Read mail',
15
+ detail: 'Search and read messages, folders, labels, and attachments.',
16
+ icon: icon('<path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/>'),
17
+ },
18
+ 'mail.drafts': {
19
+ label: 'Manage drafts',
20
+ detail: 'Create, edit, and delete drafts, and cancel scheduled sends.',
21
+ icon: icon('<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>'),
22
+ },
23
+ 'mail.organize': {
24
+ label: 'Organize mail',
25
+ detail: 'Mark read or unread, star, archive, move, and label messages.',
26
+ icon: icon('<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>'),
27
+ },
28
+ 'mail.trash': {
29
+ label: 'Use Trash',
30
+ detail: 'Move messages to Trash and restore them.',
31
+ icon: icon('<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>'),
32
+ },
33
+ 'mail.delete': {
34
+ label: 'Permanently delete mail',
35
+ detail: 'Delete messages so they cannot be recovered.',
36
+ icon: icon('<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>'),
37
+ sensitive: true,
38
+ },
39
+ 'mail.send': {
40
+ label: 'Send mail',
41
+ detail: 'Send and schedule messages from your mailboxes.',
42
+ icon: icon('<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>'),
43
+ sensitive: true,
44
+ },
45
+ };
46
+ const SHIELD_ICON = icon('<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M12 8v4"/><path d="M12 16h.01"/>');
47
+ const LOGO = `<svg viewBox="3.1 52.3 58.7 71.9" aria-hidden="true"><path fill-rule="evenodd" d="M53.299999,56.049999q-3.74,-2.92,-8.77,-2.92q-6.74,0,-11.28,5.98q-4.54,5.98,-4.59,18.67q-0.05,3.08,-0.19,7.82q-0.15,4.73,-0.32,10.15q-0.0013,0.0416,-0.0026,0.0831q-3.8057,-0.6569,-6.6374,-2.5231q-4.37,-2.88,-6.52,-7.47q-2.15,-4.59,-2.15,-9.67q0,-3.51,1.2,-7.35q1.19,-3.83,4.12,-7.25q2.93,-3.42,8.11,-5.76q-0.15,-0.49,-0.46,-1.39q-0.32,-0.9,-0.71,-1.49q-6.79,1.95,-11.01,5.03q-4.23,3.08,-6.47,6.66q-2.25,3.59,-3.08,7.11q-0.83,3.51,-0.83,6.4q0,4.1,1.44,8.2q1.44,4.1,4.64,7.54q3.2,3.44,8.52,5.52q4.0998,1.5952,9.6545,1.9611q-0.0888,2.5464,-0.1845,5.0489q-0.19,5.22,-0.44,9.4q-0.24,4.17,-0.48,6.32q0.92,1.02,1.8,1.51q1.91,-0.83,4.54,-1.71q2.64,-0.88,4.4,-1.51q-0.25,-2.69,-0.47,-7.08q-0.22,-4.39,-0.34,-9.59q-0.0305,-1.3233,-0.0598,-2.6481q5.2473,-0.749,9.7798,-3.0219q6.76,-3.39,10.72,-9.79q3.95,-6.4,3.95,-15.38q0,-4.83,-2.07,-9.37q-2.08,-4.55,-5.81,-7.48zm-16.6787,39.9042q7.1362,-0.8769,11.3287,-5.0142q5.32,-5.25,5.32,-14.91q0,-4.64,-1.71,-8.5q-1.71,-3.86,-4.32,-6.15q-2.61,-2.3,-5.44,-2.3q-1.81,0,-3.03,0.95q-1.22,0.96,-1.83,4.2q-0.61,3.25,-0.61,10.04l0,2.49q0.05,2.68,0.1,7.05q0.04,4.37,0.14,9.55q0.0251,1.2976,0.0513,2.5942z"/></svg>`;
48
+ const HTML_ENTITIES = {
49
+ '&': '&amp;',
50
+ '<': '&lt;',
51
+ '>': '&gt;',
52
+ '"': '&quot;',
53
+ "'": '&#39;',
54
+ };
55
+ export function escapeHtml(value) {
56
+ return value.replace(/[&<>"']/g, (char) => HTML_ENTITIES[char]);
57
+ }
58
+ // Matches the Fluxmail website and Cloud consent page. Inter and Newsreader are used when installed.
59
+ const STYLE = `
60
+ :root{--paper:oklch(0.988 0.002 85);--ink:oklch(0.23 0.025 268);--accent:oklch(0.51 0.195 264);--accent-strong:oklch(0.44 0.205 264);--line:oklch(0.23 0.025 268/0.08);--soft:oklch(0.23 0.025 268/0.6);color-scheme:light}
61
+ *{box-sizing:border-box}
62
+ body{margin:0;min-height:100vh;background:var(--paper) radial-gradient(60rem 28rem at 50% -8rem,oklch(0.51 0.195 264/0.1),transparent) no-repeat;color:var(--ink);font:15px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}
63
+ header{max-width:64rem;margin:0 auto;padding:20px 24px}
64
+ .brand{display:flex;width:fit-content;align-items:baseline;gap:.25em;font:24px/1.2 Newsreader,"Iowan Old Style",Georgia,serif;letter-spacing:-.02em}
65
+ .brand svg{height:.9em;width:auto;transform:translateY(.11em);fill:currentColor}
66
+ main{max-width:32rem;margin:0 auto;padding:8px 16px 64px}
67
+ .card{overflow:hidden;border:1px solid var(--line);border-radius:24px;background:oklch(1 0 0/0.85);box-shadow:0 1px 2px rgb(0 0 0/.04),0 12px 28px -16px rgb(0 0 0/.12)}
68
+ .head{padding:32px 32px 24px;text-align:center}
69
+ .logos{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:24px}
70
+ .avatar{display:flex;width:56px;height:56px;align-items:center;justify-content:center;border-radius:16px;font-size:20px;font-weight:600;box-shadow:0 0 0 1px var(--line),0 1px 2px rgb(0 0 0/.04)}
71
+ .avatar-client{background:#fff;color:oklch(0.23 0.025 268/0.7)}
72
+ .avatar-fluxmail{background:var(--accent);color:#fff}
73
+ .avatar-fluxmail svg{height:28px;width:auto;fill:currentColor}
74
+ .bridge{width:40px;border-top:2px dotted oklch(0.23 0.025 268/0.2)}
75
+ h1{margin:0;font:400 34px/1.15 Newsreader,"Iowan Old Style",Georgia,serif;letter-spacing:-.02em;text-wrap:balance;overflow-wrap:anywhere}
76
+ p{margin:0;color:var(--soft);line-height:1.6}
77
+ .head p{margin-top:12px;font-size:14px}
78
+ .head strong{font-weight:500;color:var(--ink);overflow-wrap:anywhere}
79
+ .notice{display:flex;gap:12px;margin-top:24px;padding:16px;border:1px solid oklch(0.92 0.08 85);border-radius:16px;background:oklch(0.987 0.022 95/0.7);text-align:left}
80
+ .notice svg{flex:none;width:20px;height:20px;margin-top:2px;color:oklch(0.555 0.163 49)}
81
+ .head .notice p{margin:0;color:oklch(0.28 0.07 46/0.85)}
82
+ .notice strong{color:inherit}
83
+ .section{margin:0;padding:24px 32px;border:0;border-top:1px solid var(--line);min-width:0}
84
+ .label{float:left;width:100%;margin:0;padding:0;font-size:14px;font-weight:600}
85
+ .label+*{clear:both}
86
+ .sub{margin-top:2px;font-size:14px;color:oklch(0.23 0.025 268/0.55)}
87
+ .field{display:block;margin:16px 0 6px;font-size:14px;font-weight:500}
88
+ input[type=email],input[type=password]{width:100%;padding:10px 14px;border:1px solid oklch(0.23 0.025 268/0.12);border-radius:12px;background:#fff;color:inherit;font:inherit;box-shadow:0 1px 2px rgb(0 0 0/.04)}
89
+ input[type=email]:focus,input[type=password]:focus{border-color:var(--accent);outline:none;box-shadow:0 0 0 3px oklch(0.51 0.195 264/0.15)}
90
+ input[type=checkbox],input[type=radio]{width:16px;height:16px;flex:none;margin:0;accent-color:var(--accent)}
91
+ .segment{display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-top:16px;padding:4px;border-radius:12px;background:oklch(0.23 0.025 268/0.05)}
92
+ .segment label{display:flex;cursor:pointer;align-items:center;justify-content:center;padding:8px 12px;border-radius:8px;font-size:14px;font-weight:500;color:var(--soft)}
93
+ .segment label:has(:checked){background:#fff;color:var(--ink);box-shadow:0 1px 3px rgb(0 0 0/.08)}
94
+ .segment label:has(:focus-visible){outline:2px solid var(--accent);outline-offset:2px}
95
+ .segment input{position:absolute;opacity:0;pointer-events:none}
96
+ .list{margin-top:12px;overflow:hidden;border:1px solid oklch(0.23 0.025 268/0.1);border-radius:16px;background:#fff}
97
+ .option{display:flex;cursor:pointer;align-items:center;gap:12px;padding:12px 16px;font-size:14px}
98
+ .option+.option{border-top:1px solid var(--line)}
99
+ .option:hover{background:oklch(0.23 0.025 268/0.02)}
100
+ .option-icon{display:flex;width:36px;height:36px;flex:none;align-items:center;justify-content:center;border-radius:12px;background:oklch(0.23 0.025 268/0.05);color:oklch(0.23 0.025 268/0.45)}
101
+ .option-icon svg{width:16px;height:16px}
102
+ .option:has(:checked) .option-icon{background:oklch(0.51 0.195 264/0.1);color:var(--accent)}
103
+ .option-text{display:flex;min-width:0;flex:1;flex-direction:column}
104
+ .option-text strong{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-weight:500;overflow-wrap:anywhere}
105
+ .option-text span{color:oklch(0.23 0.025 268/0.55)}
106
+ .tag{padding:1px 8px;border-radius:999px;background:oklch(0.987 0.022 95);box-shadow:0 0 0 1px oklch(0.924 0.12 95);color:oklch(0.47 0.137 46);font-size:11px;font-weight:600}
107
+ .empty{padding:12px 16px;font-size:14px}
108
+ .all-note{display:none;margin-top:12px;font-size:14px}
109
+ form:has(input[name=all_mailboxes][value="1"]:checked) .mailbox-list{display:none}
110
+ form:has(input[name=all_mailboxes][value="1"]:checked) .all-note{display:block}
111
+ .footer{padding:24px 32px;border-top:1px solid var(--line);background:oklch(0.988 0.002 85/0.7)}
112
+ .actions{display:flex;flex-direction:row-reverse;gap:12px}
113
+ button{display:inline-flex;flex:1;height:44px;cursor:pointer;align-items:center;justify-content:center;border:0;border-radius:999px;background:var(--accent);color:#fff;font:600 14px/1 inherit;font-family:inherit;box-shadow:0 1px 3px rgb(0 0 0/.08)}
114
+ button:hover{background:var(--accent-strong)}
115
+ button.secondary{border:1px solid oklch(0.23 0.025 268/0.12);background:#fff;color:var(--ink)}
116
+ button.secondary:hover{background:oklch(0.23 0.025 268/0.04)}
117
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px}
118
+ .hint{margin-top:16px;text-align:center;font-size:12px;color:oklch(0.23 0.025 268/0.55)}
119
+ .hint+.hint{margin-top:6px}
120
+ code{font:0.92em ui-monospace,SFMono-Regular,Menlo,monospace}
121
+ .error{margin-top:16px;padding:12px 16px;border-radius:12px;background:oklch(0.971 0.013 17);color:oklch(0.505 0.213 27.518);font-size:14px}
122
+ .head .error{text-align:left}
123
+ @media (max-width:480px){.head{padding:28px 24px 20px}.section,.footer{padding:20px 24px}h1{font-size:30px}}
124
+ `;
125
+ function layout(title, body) {
126
+ return (`<!doctype html><html lang="en"><head><meta charset="utf-8">` +
127
+ `<meta name="viewport" content="width=device-width,initial-scale=1">` +
128
+ `<title>${escapeHtml(title)}</title><style>${STYLE}</style></head>` +
129
+ `<body><header><div class="brand">${LOGO}<span>Fluxmail</span></div></header>` +
130
+ `<main><section class="card">${body}</section></main></body></html>`);
131
+ }
132
+ export function oauthErrorPage(title, message) {
133
+ return layout(title, `<div class="head"><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></div>`);
134
+ }
135
+ function redirectTarget(url) {
136
+ return url.protocol === 'https:' || url.protocol === 'http:' ? url.host : `${url.protocol}//`;
137
+ }
138
+ function clientIdentity(client, redirectUri) {
139
+ const url = new URL(redirectUri);
140
+ const target = redirectTarget(url);
141
+ if (client.kind === 'metadata_document' && client.metadataHost) {
142
+ return target === client.metadataHost
143
+ ? `<p>After you approve, Fluxmail returns you to <strong>${escapeHtml(target)}</strong>.</p>`
144
+ : `<p>The app's details come from <strong>${escapeHtml(client.metadataHost)}</strong>. ` +
145
+ `After you approve, Fluxmail sends you to <strong>${escapeHtml(target)}</strong>.</p>`;
146
+ }
147
+ if (isLocalRedirectUri(url)) {
148
+ // A self-reported name can't hand the connection to anyone else when the code stays on this computer.
149
+ return `<p>After you approve, Fluxmail returns you to an app on this computer.</p>`;
150
+ }
151
+ return (`<div class="notice">${SHIELD_ICON}<p>Fluxmail cannot verify who made this app. Continue only if you just ` +
152
+ `started connecting it. After you approve, Fluxmail sends you to <strong>${escapeHtml(target)}</strong>.</p></div>`);
153
+ }
154
+ function clientHead(client, redirectUri, extra = '') {
155
+ const initial = escapeHtml(client.name.trim().charAt(0).toUpperCase() || '?');
156
+ return (`<div class="head"><div class="logos" aria-hidden="true"><span class="avatar avatar-client">${initial}</span>` +
157
+ `<span class="bridge"></span><span class="avatar avatar-fluxmail">${LOGO}</span></div>` +
158
+ `<h1>Connect ${escapeHtml(client.name)} to Fluxmail</h1>${extra}${clientIdentity(client, redirectUri)}</div>`);
159
+ }
160
+ function hidden(requestId, csrf, step) {
161
+ return (`<input type="hidden" name="request_id" value="${escapeHtml(requestId)}">` +
162
+ `<input type="hidden" name="csrf" value="${escapeHtml(csrf)}">` +
163
+ `<input type="hidden" name="step" value="${step}">`);
164
+ }
165
+ const EXPIRY_NOTE = '<p class="hint">This request expires 10 minutes after it starts.</p>';
166
+ function errorNote(error) {
167
+ return error ? `<p class="error" role="alert">${escapeHtml(error)}</p>` : '';
168
+ }
169
+ export function oauthSignInPage(input) {
170
+ return layout(`Connect ${input.client.name} to Fluxmail`, clientHead(input.client, input.redirectUri) +
171
+ `<form method="post" action="${escapeHtml(input.action)}">${hidden(input.requestId, input.csrf, 'sign_in')}` +
172
+ `<div class="section"><p class="sub">Sign in with your Fluxmail member account.</p>${errorNote(input.error)}` +
173
+ `<label class="field" for="email">Email</label>` +
174
+ `<input id="email" name="email" type="email" autocomplete="username" required value="${escapeHtml(input.email ?? '')}">` +
175
+ `<label class="field" for="password">Password</label>` +
176
+ `<input id="password" name="password" type="password" autocomplete="current-password" required></div>` +
177
+ // The primary button comes first so pressing Enter continues; the row is reversed for display.
178
+ `<div class="footer"><div class="actions"><button type="submit" name="action" value="continue">Continue</button>` +
179
+ `<button class="secondary" type="submit" name="action" value="deny" formnovalidate>Cancel</button></div>` +
180
+ `${EXPIRY_NOTE}</div></form>`);
181
+ }
182
+ export function oauthApprovePage(input) {
183
+ // Permanent deletion is opt-in even when the client asks for it.
184
+ const checkedScopes = new Set(input.checkedScopes ?? input.scopes.filter((scope) => scope !== 'mail.delete'));
185
+ const checkedAccounts = new Set(input.checkedAccountIds ?? (input.accounts.length === 1 ? [input.accounts[0].id] : []));
186
+ const scopeOptions = input.scopes
187
+ .map((scope) => {
188
+ const { label, detail, icon: scopeIcon, sensitive } = SCOPE_LABELS[scope];
189
+ return (`<label class="option"><span class="option-icon">${scopeIcon}</span><span class="option-text">` +
190
+ `<strong>${label}${sensitive ? '<span class="tag">Sensitive</span>' : ''}</strong><span>${detail}</span></span>` +
191
+ `<input type="checkbox" name="scope" value="${scope}"${checkedScopes.has(scope) ? ' checked' : ''}></label>`);
192
+ })
193
+ .join('');
194
+ const accountOptions = input.accounts
195
+ .map((account) => `<label class="option"><input type="checkbox" name="account" value="${escapeHtml(account.id)}"${checkedAccounts.has(account.id) ? ' checked' : ''}><span class="option-text"><strong>${escapeHtml(account.email)}</strong></span></label>`)
196
+ .join('');
197
+ const all = Boolean(input.allMailboxes);
198
+ return layout(`Connect ${input.client.name} to Fluxmail`, clientHead(input.client, input.redirectUri, `<p>Signed in as <strong>${escapeHtml(input.memberName)}</strong>.</p>${errorNote(input.error)}`) +
199
+ `<form method="post" action="${escapeHtml(input.action)}">${hidden(input.requestId, input.csrf, 'approve')}` +
200
+ `<fieldset class="section"><legend class="label">Mailboxes</legend>` +
201
+ `<p class="sub">Choose which mailboxes this app can use.</p>` +
202
+ `<div class="segment"><label><input type="radio" name="all_mailboxes" value="0"${all ? '' : ' checked'}>Selected</label>` +
203
+ `<label><input type="radio" name="all_mailboxes" value="1"${all ? ' checked' : ''}>All mailboxes</label></div>` +
204
+ `<div class="list mailbox-list">${accountOptions || '<p class="empty">You do not have any mailboxes yet.</p>'}</div>` +
205
+ `<p class="all-note">Includes every mailbox you can use now, plus any you get access to later.</p></fieldset>` +
206
+ `<fieldset class="section"><legend class="label">Permissions</legend>` +
207
+ `<p class="sub">Uncheck anything you don't want this app to do.</p>` +
208
+ `<div class="list">${scopeOptions}</div></fieldset>` +
209
+ `<div class="footer"><div class="actions"><button type="submit" name="action" value="approve">Approve</button>` +
210
+ `<button class="secondary" type="submit" name="action" value="deny">Deny</button></div>` +
211
+ `${EXPIRY_NOTE}<p class="hint">You can disconnect this app later with <code>fluxmail connections revoke</code>.</p></div></form>`);
212
+ }
213
+ //# sourceMappingURL=oauthPages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthPages.js","sourceRoot":"","sources":["../../src/http/oauthPages.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/http/rest.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,WAAW,EAAK,MAAM,mBAAmB,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAYtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAqB,KAAK,YAAY,EAAkB,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAA2C,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAIrF,OAAO,EAA2C,KAAK,SAAS,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAMpH,OAAO,EAA+B,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAGzE,UAAU,aAAa;IACrB,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,EAAE,YAAY,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,KAAK,OAAO,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,aAAa,CAAA;CAAE,CAAC;AAGpE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,oBAAoB,CAAC;IACpC,EAAE,EAAE,UAAU,CAAC;IACf,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAkWD,eAAO,MAAM,qBAAqB;;;;;;;CAOjC,CAAC;AA+pBF,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,CAm/BrE"}
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/http/rest.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,WAAW,EAAK,MAAM,mBAAmB,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAYtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAqB,KAAK,YAAY,EAAkB,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAA2C,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAIrF,OAAO,EAA2C,KAAK,SAAS,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAMpH,OAAO,EAA+B,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AASzE,UAAU,aAAa;IACrB,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,EAAE,YAAY,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,KAAK,OAAO,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,aAAa,CAAA;CAAE,CAAC;AAGpE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,oBAAoB,CAAC;IACpC,EAAE,EAAE,UAAU,CAAC;IACf,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAkWD,eAAO,MAAM,qBAAqB;;;;;;;CAOjC,CAAC;AAiqBF,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,CAq/BrE"}
package/dist/http/rest.js CHANGED
@@ -15,7 +15,7 @@ import { recordAdminAuditEvent } from '../storage/adminAudit.js';
15
15
  import { identityOperationId, registerIdentityRoutes } from './identity.js';
16
16
  import { operationProperties } from './operationTelemetry.js';
17
17
  import { logCodedFailure, logFailure } from '../logging.js';
18
- import { ClientInputError, MissingCapabilityError, publicError } from '../service/publicErrors.js';
18
+ import { attachmentTooLarge, ClientInputError, isAttachmentSizeError, MissingCapabilityError, publicError, } from '../service/publicErrors.js';
19
19
  const id = z.string().trim().min(1);
20
20
  const accountId = id.openapi({ example: 'acct_123' });
21
21
  const messageId = id.openapi({ example: 'msg_123' });
@@ -805,12 +805,16 @@ async function runAttachment(c, deps, accountId, messageId, attachmentId) {
805
805
  }
806
806
  const service = c.get('restService');
807
807
  const warning = service.enforceQuota();
808
- const { meta, content } = await service.getAttachment(accountId, messageId, attachmentId, deps.config.maxAttachmentBytes);
808
+ const { meta, content } = await service
809
+ .getAttachment(accountId, messageId, attachmentId, deps.config.maxAttachmentBytes)
810
+ .catch((error) => {
811
+ if (!isAttachmentSizeError(error))
812
+ throw error;
813
+ const sizeBytes = error.data?.sizeBytes;
814
+ throw attachmentTooLarge('the REST API', deps.config.maxAttachmentBytes, typeof sizeBytes === 'number' ? sizeBytes : undefined);
815
+ });
809
816
  if (content.length > deps.config.maxAttachmentBytes) {
810
- throw new ClientInputError('invalid_request', 'Attachment is too large to return through the REST API.', {
811
- sizeBytes: content.length,
812
- maxBytes: deps.config.maxAttachmentBytes,
813
- });
817
+ throw attachmentTooLarge('the REST API', deps.config.maxAttachmentBytes, content.length);
814
818
  }
815
819
  const safeName = safeAttachmentFilename(meta.filename);
816
820
  const asciiName = safeName.replace(/[^\x20-\x7e]/g, '_');
@@ -1145,7 +1149,9 @@ export function createRestApi(deps) {
1145
1149
  }
1146
1150
  const authorization = c.req.header('authorization');
1147
1151
  const bearer = authorization?.match(/^Bearer\s+([^\s]+)$/i)?.[1];
1148
- const auth = bearer ? authenticateBearer(deps.db, bearer) : null;
1152
+ // OAuth access tokens are issued for the MCP endpoint only.
1153
+ const principal = bearer ? authenticateBearer(deps.db, bearer) : null;
1154
+ const auth = principal?.kind === 'oauth' ? null : principal;
1149
1155
  if (!auth) {
1150
1156
  logCodedFailure(deps.logger, 'rest.request_rejected', 'unauthorized', 'REST request was not authorized', {
1151
1157
  productSurface: 'rest',