@ggui-ai/mcp-server 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/admin-blueprints-transport.d.ts +114 -0
- package/dist/admin-blueprints-transport.d.ts.map +1 -0
- package/dist/admin-blueprints-transport.js +118 -0
- package/dist/admin-oauth-providers-transport.d.ts +40 -0
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
- package/dist/admin-oauth-providers-transport.js +263 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +75 -0
- package/dist/build-mcp.d.ts +128 -0
- package/dist/build-mcp.d.ts.map +1 -0
- package/dist/build-mcp.js +113 -0
- package/dist/code-store-fs.d.ts +19 -0
- package/dist/code-store-fs.d.ts.map +1 -0
- package/dist/code-store-fs.js +98 -0
- package/dist/console-auth.d.ts +139 -0
- package/dist/console-auth.d.ts.map +1 -0
- package/dist/console-auth.js +102 -0
- package/dist/console-cache.d.ts +78 -0
- package/dist/console-cache.d.ts.map +1 -0
- package/dist/console-cache.js +105 -0
- package/dist/console-headers.d.ts +124 -0
- package/dist/console-headers.d.ts.map +1 -0
- package/dist/console-headers.js +49 -0
- package/dist/console-llm-trace.d.ts +66 -0
- package/dist/console-llm-trace.d.ts.map +1 -0
- package/dist/console-llm-trace.js +105 -0
- package/dist/console-payloads.d.ts +67 -0
- package/dist/console-payloads.d.ts.map +1 -0
- package/dist/console-payloads.js +105 -0
- package/dist/console-theme-routes.d.ts +111 -0
- package/dist/console-theme-routes.d.ts.map +1 -0
- package/dist/console-theme-routes.js +202 -0
- package/dist/console-timeline.d.ts +45 -0
- package/dist/console-timeline.d.ts.map +1 -0
- package/dist/console-timeline.js +169 -0
- package/dist/console-validator.d.ts +67 -0
- package/dist/console-validator.d.ts.map +1 -0
- package/dist/console-validator.js +105 -0
- package/dist/console-welcome.d.ts +7 -0
- package/dist/console-welcome.d.ts.map +1 -0
- package/dist/console-welcome.js +221 -0
- package/dist/csrf-middleware.d.ts +55 -0
- package/dist/csrf-middleware.d.ts.map +1 -0
- package/dist/csrf-middleware.js +138 -0
- package/dist/email-login.d.ts +174 -0
- package/dist/email-login.d.ts.map +1 -0
- package/dist/email-login.js +254 -0
- package/dist/email-resend.d.ts +29 -0
- package/dist/email-resend.d.ts.map +1 -0
- package/dist/email-resend.js +71 -0
- package/dist/email-sender-from-env.d.ts +34 -0
- package/dist/email-sender-from-env.d.ts.map +1 -0
- package/dist/email-sender-from-env.js +112 -0
- package/dist/email-smtp.d.ts +42 -0
- package/dist/email-smtp.d.ts.map +1 -0
- package/dist/email-smtp.js +81 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/instructions-presets.d.ts +112 -0
- package/dist/instructions-presets.d.ts.map +1 -0
- package/dist/instructions-presets.js +195 -0
- package/dist/llm-backed-negotiator.d.ts +178 -0
- package/dist/llm-backed-negotiator.d.ts.map +1 -0
- package/dist/llm-backed-negotiator.js +579 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/mcp-apps-inbound.d.ts +86 -0
- package/dist/mcp-apps-inbound.d.ts.map +1 -0
- package/dist/mcp-apps-inbound.js +278 -0
- package/dist/mcp-apps-outbound.d.ts +448 -0
- package/dist/mcp-apps-outbound.d.ts.map +1 -0
- package/dist/mcp-apps-outbound.js +1163 -0
- package/dist/mcp-mounts.d.ts +239 -0
- package/dist/mcp-mounts.d.ts.map +1 -0
- package/dist/mcp-mounts.js +222 -0
- package/dist/oauth-login-types.d.ts +160 -0
- package/dist/oauth-login-types.d.ts.map +1 -0
- package/dist/oauth-login-types.js +9 -0
- package/dist/oauth-login.d.ts +77 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +455 -0
- package/dist/oauth-providers/github.d.ts +17 -0
- package/dist/oauth-providers/github.d.ts.map +1 -0
- package/dist/oauth-providers/github.js +89 -0
- package/dist/oauth-providers/google.d.ts +18 -0
- package/dist/oauth-providers/google.d.ts.map +1 -0
- package/dist/oauth-providers/google.js +59 -0
- package/dist/oauth-providers-store.d.ts +32 -0
- package/dist/oauth-providers-store.d.ts.map +1 -0
- package/dist/oauth-providers-store.js +291 -0
- package/dist/oauth.d.ts +347 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +686 -0
- package/dist/pairing-transport.d.ts +99 -0
- package/dist/pairing-transport.d.ts.map +1 -0
- package/dist/pairing-transport.js +223 -0
- package/dist/rate-limit-middleware.d.ts +36 -0
- package/dist/rate-limit-middleware.d.ts.map +1 -0
- package/dist/rate-limit-middleware.js +57 -0
- package/dist/render-gate.d.ts +87 -0
- package/dist/render-gate.d.ts.map +1 -0
- package/dist/render-gate.js +77 -0
- package/dist/render-rate-limit.d.ts +59 -0
- package/dist/render-rate-limit.d.ts.map +1 -0
- package/dist/render-rate-limit.js +73 -0
- package/dist/render-signing.d.ts +98 -0
- package/dist/render-signing.d.ts.map +1 -0
- package/dist/render-signing.js +113 -0
- package/dist/request-context.d.ts +113 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +154 -0
- package/dist/reserved-validators.d.ts +22 -0
- package/dist/reserved-validators.d.ts.map +1 -0
- package/dist/reserved-validators.js +101 -0
- package/dist/schema-compat.d.ts +167 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +187 -0
- package/dist/security-headers-middleware.d.ts +38 -0
- package/dist/security-headers-middleware.d.ts.map +1 -0
- package/dist/security-headers-middleware.js +30 -0
- package/dist/server.d.ts +2060 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +6338 -0
- package/dist/session-channel.d.ts +651 -0
- package/dist/session-channel.d.ts.map +1 -0
- package/dist/session-channel.js +1756 -0
- package/dist/storage.d.ts +89 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +171 -0
- package/dist/thread-transport.d.ts +118 -0
- package/dist/thread-transport.d.ts.map +1 -0
- package/dist/thread-transport.js +478 -0
- package/dist/user-session-auth.d.ts +167 -0
- package/dist/user-session-auth.d.ts.map +1 -0
- package/dist/user-session-auth.js +148 -0
- package/package.json +76 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { formatUserSessionCookieHeader } from './user-session-auth.js';
|
|
3
|
+
import { createConsoleLogger } from './logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Public route paths. Operators may override via
|
|
6
|
+
* `EmailLoginRoutesOptions.{startPath,verifyPath,configPath}` for
|
|
7
|
+
* test or sub-mount scenarios.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_EMAIL_LOGIN_START_PATH = '/ggui/email-login/start';
|
|
10
|
+
export const DEFAULT_EMAIL_LOGIN_VERIFY_PATH = '/ggui/email-login/verify';
|
|
11
|
+
export const DEFAULT_EMAIL_LOGIN_CONFIG_PATH = '/ggui/email-login/config';
|
|
12
|
+
const TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
13
|
+
const DEFAULT_NEXT_PATH = '/settings';
|
|
14
|
+
/**
|
|
15
|
+
* Fallback sender that logs every "sent" email to the configured
|
|
16
|
+
* logger at info level. The verify URL appears in the logs so an
|
|
17
|
+
* operator running `ggui serve` locally can copy/paste it without
|
|
18
|
+
* setting up real email infrastructure. Production deploys MUST
|
|
19
|
+
* swap this for a real sender — the fallback is a developer-mode
|
|
20
|
+
* convenience, not a security control.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export class ConsoleEmailSender {
|
|
25
|
+
logger;
|
|
26
|
+
constructor(logger) {
|
|
27
|
+
this.logger = logger ?? createConsoleLogger({ component: 'email-console-sender' });
|
|
28
|
+
}
|
|
29
|
+
async send(message) {
|
|
30
|
+
this.logger.info('email_console_sender', {
|
|
31
|
+
to: message.to,
|
|
32
|
+
from: message.from ?? '<default>',
|
|
33
|
+
subject: message.subject,
|
|
34
|
+
text: message.text,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Default in-memory store. Loses tokens on process restart (which
|
|
40
|
+
* is acceptable — pending magic links expire in 15 min anyway, and
|
|
41
|
+
* a restart is itself a "request a new link" prompt to the user).
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export class InMemoryMagicLinkStore {
|
|
46
|
+
records = new Map();
|
|
47
|
+
async mintToken({ email, nextPath, ttlMs }) {
|
|
48
|
+
// 32 random bytes → 64 hex chars. Plenty of entropy; no need
|
|
49
|
+
// for the more compact base64url since hex copy/pastes cleaner
|
|
50
|
+
// out of email clients that try to "smarten" punctuation.
|
|
51
|
+
const token = randomBytes(32).toString('hex');
|
|
52
|
+
this.records.set(token, {
|
|
53
|
+
email,
|
|
54
|
+
nextPath,
|
|
55
|
+
expiresAt: Date.now() + ttlMs,
|
|
56
|
+
});
|
|
57
|
+
return token;
|
|
58
|
+
}
|
|
59
|
+
async consumeToken(token) {
|
|
60
|
+
const r = this.records.get(token);
|
|
61
|
+
if (!r)
|
|
62
|
+
return null;
|
|
63
|
+
// Single-use: delete BEFORE checking expiry so an expired but
|
|
64
|
+
// present token can't be reconsumed by a clock-rewind attacker.
|
|
65
|
+
this.records.delete(token);
|
|
66
|
+
if (Date.now() > r.expiresAt)
|
|
67
|
+
return null;
|
|
68
|
+
return { email: r.email, nextPath: r.nextPath };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const DEFAULT_SUBJECT = 'Sign in to ggui';
|
|
72
|
+
const DEFAULT_BODY_TEXT = "Click the link below to sign in. It expires in 15 minutes.\n\n" +
|
|
73
|
+
'{verifyUrl}\n\n' +
|
|
74
|
+
"If you didn't request this, you can safely ignore this email.";
|
|
75
|
+
const DEFAULT_BODY_HTML = '<p>Click the link below to sign in. It expires in 15 minutes.</p>' +
|
|
76
|
+
'<p><a href="{verifyUrl}">{verifyUrl}</a></p>' +
|
|
77
|
+
"<p style=\"color:#888\">If you didn't request this, you can safely ignore this email.</p>";
|
|
78
|
+
/**
|
|
79
|
+
* Loose RFC 5322 subset — `local@host.tld`. Doesn't reject every
|
|
80
|
+
* weird-but-legal address (no IDN, no quoted locals), just rules
|
|
81
|
+
* out obviously-broken input early. The downstream SMTP/HTTP
|
|
82
|
+
* sender is the authoritative validator.
|
|
83
|
+
*/
|
|
84
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
85
|
+
/**
|
|
86
|
+
* Mount `POST /start`, `GET /verify`, `GET /config`. Idempotent
|
|
87
|
+
* across multiple calls (later mount wins for the same path).
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
export function mountEmailLoginRoutes(app, opts) {
|
|
92
|
+
const startPath = opts.startPath ?? DEFAULT_EMAIL_LOGIN_START_PATH;
|
|
93
|
+
const verifyPath = opts.verifyPath ?? DEFAULT_EMAIL_LOGIN_VERIFY_PATH;
|
|
94
|
+
const configPath = opts.configPath ?? DEFAULT_EMAIL_LOGIN_CONFIG_PATH;
|
|
95
|
+
const store = opts.store ?? new InMemoryMagicLinkStore();
|
|
96
|
+
const subject = opts.subject ?? DEFAULT_SUBJECT;
|
|
97
|
+
const bodyText = opts.bodyText ?? DEFAULT_BODY_TEXT;
|
|
98
|
+
const bodyHtml = opts.bodyHtml ?? DEFAULT_BODY_HTML;
|
|
99
|
+
const auditSink = opts.auditSink;
|
|
100
|
+
const emitAudit = async (entry, auditLogger) => {
|
|
101
|
+
if (!auditSink)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
await auditSink.record({ at: Date.now(), ...entry });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
auditLogger.warn('audit_emit_failed', {
|
|
108
|
+
action: entry.action,
|
|
109
|
+
error: String(err),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// --- GET /ggui/email-login/config ---
|
|
114
|
+
// Public + cheap. /login fetches this to know whether to render
|
|
115
|
+
// the email form. No secrets here — just a presence boolean.
|
|
116
|
+
app.get(configPath, (_req, res) => {
|
|
117
|
+
res.status(200).json({ enabled: true });
|
|
118
|
+
});
|
|
119
|
+
// --- POST /ggui/email-login/start ---
|
|
120
|
+
// Body: { email: string, next?: string }. Always 200 (avoid
|
|
121
|
+
// email enumeration — see route docstring for why).
|
|
122
|
+
app.post(startPath, async (req, res) => {
|
|
123
|
+
const reqLogger = opts.logger.child({ route: 'POST ' + startPath });
|
|
124
|
+
const body = typeof req.body === 'object' && req.body !== null
|
|
125
|
+
? req.body
|
|
126
|
+
: {};
|
|
127
|
+
const rawEmail = typeof body['email'] === 'string' ? body['email'] : '';
|
|
128
|
+
const email = rawEmail.trim().toLowerCase();
|
|
129
|
+
if (!EMAIL_RE.test(email) || email.length > 254) {
|
|
130
|
+
// 254 = RFC 5321 max length. Reject malformed up-front — this
|
|
131
|
+
// is "user typo" not "attacker probe", so 400 is honest.
|
|
132
|
+
reqLogger.warn('email_login_invalid_email', {});
|
|
133
|
+
res.status(400).json({
|
|
134
|
+
error: {
|
|
135
|
+
code: 'invalid_email',
|
|
136
|
+
message: 'Provide a valid email address.',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const nextRaw = typeof body['next'] === 'string' ? body['next'] : undefined;
|
|
142
|
+
const nextPath = sanitizeNextPath(nextRaw) ?? DEFAULT_NEXT_PATH;
|
|
143
|
+
const token = await store.mintToken({
|
|
144
|
+
email,
|
|
145
|
+
nextPath,
|
|
146
|
+
ttlMs: TOKEN_TTL_MS,
|
|
147
|
+
});
|
|
148
|
+
const verifyUrl = `${trimTrailingSlash(opts.publicBaseUrl)}${verifyPath}` +
|
|
149
|
+
`?token=${encodeURIComponent(token)}`;
|
|
150
|
+
try {
|
|
151
|
+
await opts.sender.send({
|
|
152
|
+
to: email,
|
|
153
|
+
from: opts.fromAddress,
|
|
154
|
+
subject,
|
|
155
|
+
text: bodyText.replace(/\{verifyUrl\}/g, verifyUrl),
|
|
156
|
+
html: bodyHtml.replace(/\{verifyUrl\}/g, verifyUrl),
|
|
157
|
+
});
|
|
158
|
+
reqLogger.info('email_login_sent', { email });
|
|
159
|
+
await emitAudit({
|
|
160
|
+
action: 'auth.email.start',
|
|
161
|
+
actor: { kind: 'anonymous' },
|
|
162
|
+
metadata: { email },
|
|
163
|
+
}, reqLogger);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// Sender failures we log + audit, but we still 200 to the
|
|
167
|
+
// caller — same enumeration concern. The user's "I never got
|
|
168
|
+
// an email" is the discovery path; ops finds it in logs.
|
|
169
|
+
reqLogger.warn('email_login_send_failed', {
|
|
170
|
+
email,
|
|
171
|
+
error: String(err),
|
|
172
|
+
});
|
|
173
|
+
await emitAudit({
|
|
174
|
+
action: 'auth.email.failure',
|
|
175
|
+
actor: { kind: 'anonymous' },
|
|
176
|
+
metadata: { email, reason: 'send_failed', detail: String(err) },
|
|
177
|
+
}, reqLogger);
|
|
178
|
+
}
|
|
179
|
+
res.status(200).json({ ok: true });
|
|
180
|
+
});
|
|
181
|
+
// --- GET /ggui/email-login/verify?token=... ---
|
|
182
|
+
app.get(verifyPath, async (req, res) => {
|
|
183
|
+
const reqLogger = opts.logger.child({ route: 'GET ' + verifyPath });
|
|
184
|
+
const tokenRaw = req.query['token'];
|
|
185
|
+
const token = typeof tokenRaw === 'string' ? tokenRaw : '';
|
|
186
|
+
if (!token) {
|
|
187
|
+
reqLogger.warn('email_login_verify_missing_token', {});
|
|
188
|
+
res.status(400).send('Missing token. Open your email and click the link again.');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const consumed = await store.consumeToken(token);
|
|
192
|
+
if (!consumed) {
|
|
193
|
+
reqLogger.warn('email_login_verify_invalid', {});
|
|
194
|
+
await emitAudit({
|
|
195
|
+
action: 'auth.email.failure',
|
|
196
|
+
actor: { kind: 'anonymous' },
|
|
197
|
+
metadata: { reason: 'invalid_or_expired_token' },
|
|
198
|
+
}, reqLogger);
|
|
199
|
+
res
|
|
200
|
+
.status(403)
|
|
201
|
+
.send('This sign-in link is invalid or has expired. ' +
|
|
202
|
+
'Request a new link from /login.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!opts.auth.registerToken) {
|
|
206
|
+
reqLogger.warn('email_login_register_token_unsupported', {});
|
|
207
|
+
res.status(501).json({
|
|
208
|
+
error: {
|
|
209
|
+
code: 'not_supported',
|
|
210
|
+
message: 'AuthAdapter has no registerToken — email login requires a token-registering adapter.',
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const userId = `email:${consumed.email}`;
|
|
216
|
+
const bearer = `ggui_user_${randomBytes(24).toString('hex')}`;
|
|
217
|
+
opts.auth.registerToken(bearer, {
|
|
218
|
+
identity: { kind: 'user', userId, roles: [] },
|
|
219
|
+
source: 'email',
|
|
220
|
+
metadata: { email: consumed.email },
|
|
221
|
+
});
|
|
222
|
+
const sessionCookie = formatUserSessionCookieHeader({
|
|
223
|
+
bearer,
|
|
224
|
+
...(opts.ttlSec !== undefined ? { ttlSec: opts.ttlSec } : {}),
|
|
225
|
+
...(opts.secure !== undefined ? { secure: opts.secure } : {}),
|
|
226
|
+
});
|
|
227
|
+
res.setHeader('Set-Cookie', sessionCookie);
|
|
228
|
+
reqLogger.info('email_login_success', { userId });
|
|
229
|
+
await emitAudit({
|
|
230
|
+
action: 'auth.email.success',
|
|
231
|
+
actor: { kind: 'user', id: userId },
|
|
232
|
+
metadata: { email: consumed.email },
|
|
233
|
+
}, reqLogger);
|
|
234
|
+
res.redirect(302, consumed.nextPath);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Reject open-redirects: only same-origin RELATIVE paths starting
|
|
239
|
+
* with `/` (and NOT `//` which the URL parser treats as protocol-
|
|
240
|
+
* relative). Returns null when the input fails validation so the
|
|
241
|
+
* caller can pick the default.
|
|
242
|
+
*/
|
|
243
|
+
function sanitizeNextPath(raw) {
|
|
244
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
245
|
+
return null;
|
|
246
|
+
if (!raw.startsWith('/'))
|
|
247
|
+
return null;
|
|
248
|
+
if (raw.startsWith('//'))
|
|
249
|
+
return null;
|
|
250
|
+
return raw;
|
|
251
|
+
}
|
|
252
|
+
function trimTrailingSlash(s) {
|
|
253
|
+
return s.endsWith('/') ? s.slice(0, -1) : s;
|
|
254
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EmailMessage, EmailSender } from './email-login.js';
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface ResendEmailSenderOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Resend API key, format `re_...`. Mint at
|
|
9
|
+
* https://resend.com/api-keys. Required.
|
|
10
|
+
*/
|
|
11
|
+
readonly apiKey: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional logger. Defaults to a component-bound console logger.
|
|
14
|
+
* Receives one `email_resend_sent` event per successful send and
|
|
15
|
+
* one `email_resend_failed` event per failure (with `errorCode`
|
|
16
|
+
* + `errorMessage` from the SDK response).
|
|
17
|
+
*/
|
|
18
|
+
readonly logger?: Logger;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export declare class ResendEmailSender implements EmailSender {
|
|
24
|
+
private readonly client;
|
|
25
|
+
private readonly logger;
|
|
26
|
+
constructor(opts: ResendEmailSenderOptions);
|
|
27
|
+
send(message: EmailMessage): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=email-resend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-resend.d.ts","sourceRoot":"","sources":["../src/email-resend.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,qBAAa,iBAAkB,YAAW,WAAW;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,IAAI,EAAE,wBAAwB;IAMpC,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAiCjD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend-backed `EmailSender`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the `resend` SDK (https://resend.com) into the same
|
|
5
|
+
* `EmailSender` interface as `ConsoleEmailSender` — drop-in
|
|
6
|
+
* replacement: construct it, pass it to `createGguiServer({emailLogin: {sender, …}})`.
|
|
7
|
+
*
|
|
8
|
+
* The OSS CLI auto-constructs this when `GGUI_EMAIL_SENDER=resend` and
|
|
9
|
+
* `RESEND_API_KEY` are set in the environment. Programmatic embedders
|
|
10
|
+
* import it directly:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { ResendEmailSender, createGguiServer } from '@ggui-ai/mcp-server';
|
|
14
|
+
*
|
|
15
|
+
* const server = createGguiServer({
|
|
16
|
+
* emailLogin: {
|
|
17
|
+
* sender: new ResendEmailSender({ apiKey: process.env.RESEND_API_KEY! }),
|
|
18
|
+
* fromAddress: 'Acme <noreply@acme.com>',
|
|
19
|
+
* },
|
|
20
|
+
* // ...
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Note: Resend requires the `from` address's domain to be verified
|
|
25
|
+
* in the Resend dashboard. Pass an unverified domain and `send()`
|
|
26
|
+
* will reject — the adapter surfaces the SDK error verbatim, no
|
|
27
|
+
* silent swallow. See https://resend.com/docs/dashboard/domains/introduction.
|
|
28
|
+
*/
|
|
29
|
+
import { Resend } from 'resend';
|
|
30
|
+
import { createConsoleLogger } from './logger.js';
|
|
31
|
+
/**
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export class ResendEmailSender {
|
|
35
|
+
client;
|
|
36
|
+
logger;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.client = new Resend(opts.apiKey);
|
|
39
|
+
this.logger =
|
|
40
|
+
opts.logger ?? createConsoleLogger({ component: 'email-resend-sender' });
|
|
41
|
+
}
|
|
42
|
+
async send(message) {
|
|
43
|
+
if (!message.from) {
|
|
44
|
+
// Resend requires an explicit `from`. Defer the surface error
|
|
45
|
+
// to the caller — `mountEmailLoginRoutes` always supplies one
|
|
46
|
+
// from `fromAddress`, so this branch is operator-config error.
|
|
47
|
+
throw new Error('ResendEmailSender: `from` address required (set `emailLogin.fromAddress` on createGguiServer).');
|
|
48
|
+
}
|
|
49
|
+
const { data, error } = await this.client.emails.send({
|
|
50
|
+
from: message.from,
|
|
51
|
+
to: message.to,
|
|
52
|
+
subject: message.subject,
|
|
53
|
+
text: message.text,
|
|
54
|
+
...(message.html ? { html: message.html } : {}),
|
|
55
|
+
});
|
|
56
|
+
if (error) {
|
|
57
|
+
this.logger.warn('email_resend_failed', {
|
|
58
|
+
to: message.to,
|
|
59
|
+
from: message.from,
|
|
60
|
+
errorName: error.name,
|
|
61
|
+
errorMessage: error.message,
|
|
62
|
+
});
|
|
63
|
+
throw new Error(`ResendEmailSender.send failed: ${error.name}: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
this.logger.info('email_resend_sent', {
|
|
66
|
+
to: message.to,
|
|
67
|
+
from: message.from,
|
|
68
|
+
messageId: data?.id,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EmailSender } from './email-login.js';
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export type EmailSenderKind = 'console' | 'resend' | 'smtp';
|
|
7
|
+
/**
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type EmailSenderSelection = {
|
|
11
|
+
kind: 'ok';
|
|
12
|
+
sender: EmailSender;
|
|
13
|
+
senderKind: EmailSenderKind;
|
|
14
|
+
/** Operator-supplied `GGUI_EMAIL_FROM`, if set. CLI uses this to override the default `fromAddress`. */
|
|
15
|
+
fromAddress?: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'error';
|
|
18
|
+
senderKind: EmailSenderKind;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export interface SelectEmailSenderOptions {
|
|
25
|
+
/** Process env. Defaults to `process.env`. */
|
|
26
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
27
|
+
/** Optional logger threaded into the constructed sender. */
|
|
28
|
+
readonly logger?: Logger;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export declare function selectEmailSenderFromEnv(opts?: SelectEmailSenderOptions): EmailSenderSelection;
|
|
34
|
+
//# sourceMappingURL=email-sender-from-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-sender-from-env.d.ts","sourceRoot":"","sources":["../src/email-sender-from-env.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5D;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,wGAAwG;IACxG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,GAAE,wBAA6B,GAClC,oBAAoB,CA0FtB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env-driven `EmailSender` selector. Lets `ggui serve` (or any
|
|
3
|
+
* embedder reading from process.env) pick between the three built-in
|
|
4
|
+
* senders without hardcoding branches.
|
|
5
|
+
*
|
|
6
|
+
* Env contract:
|
|
7
|
+
*
|
|
8
|
+
* - `GGUI_EMAIL_SENDER` — `console` | `resend` | `smtp` (default: `console`)
|
|
9
|
+
* - `GGUI_EMAIL_FROM` — overrides the default `fromAddress` (returned alongside the sender)
|
|
10
|
+
* - `RESEND_API_KEY` — required when `GGUI_EMAIL_SENDER=resend`
|
|
11
|
+
* - `SMTP_URL` — connection string, e.g. `smtps://user:pass@host:465`
|
|
12
|
+
* - `SMTP_HOST` / `SMTP_PORT` — discrete config (alternative to `SMTP_URL`)
|
|
13
|
+
* - `SMTP_USER` / `SMTP_PASS` — auth (used with `SMTP_HOST`)
|
|
14
|
+
* - `SMTP_SECURE` — `'true'`/`'false'` to override the auto-derive
|
|
15
|
+
*
|
|
16
|
+
* Misconfiguration (e.g. `GGUI_EMAIL_SENDER=resend` without
|
|
17
|
+
* `RESEND_API_KEY`) returns `{ kind: 'error', reason }` so the CLI
|
|
18
|
+
* can log + fall back to console rather than crashing the boot.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
import { ConsoleEmailSender } from './email-login.js';
|
|
23
|
+
import { ResendEmailSender } from './email-resend.js';
|
|
24
|
+
import { SmtpEmailSender } from './email-smtp.js';
|
|
25
|
+
/**
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export function selectEmailSenderFromEnv(opts = {}) {
|
|
29
|
+
const env = opts.env ?? process.env;
|
|
30
|
+
const raw = env.GGUI_EMAIL_SENDER?.trim().toLowerCase() ?? 'console';
|
|
31
|
+
const fromAddress = env.GGUI_EMAIL_FROM?.trim() || undefined;
|
|
32
|
+
if (raw !== 'console' && raw !== 'resend' && raw !== 'smtp') {
|
|
33
|
+
return {
|
|
34
|
+
kind: 'error',
|
|
35
|
+
senderKind: 'console',
|
|
36
|
+
reason: `GGUI_EMAIL_SENDER='${raw}' is not one of console|resend|smtp`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const senderKind = raw;
|
|
40
|
+
if (senderKind === 'console') {
|
|
41
|
+
const result = {
|
|
42
|
+
kind: 'ok',
|
|
43
|
+
sender: new ConsoleEmailSender(opts.logger),
|
|
44
|
+
senderKind,
|
|
45
|
+
};
|
|
46
|
+
return fromAddress ? { ...result, fromAddress } : result;
|
|
47
|
+
}
|
|
48
|
+
if (senderKind === 'resend') {
|
|
49
|
+
const apiKey = env.RESEND_API_KEY?.trim();
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
return {
|
|
52
|
+
kind: 'error',
|
|
53
|
+
senderKind,
|
|
54
|
+
reason: 'GGUI_EMAIL_SENDER=resend but RESEND_API_KEY is not set',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const result = {
|
|
58
|
+
kind: 'ok',
|
|
59
|
+
sender: new ResendEmailSender({
|
|
60
|
+
apiKey,
|
|
61
|
+
...(opts.logger ? { logger: opts.logger } : {}),
|
|
62
|
+
}),
|
|
63
|
+
senderKind,
|
|
64
|
+
};
|
|
65
|
+
return fromAddress ? { ...result, fromAddress } : result;
|
|
66
|
+
}
|
|
67
|
+
// smtp
|
|
68
|
+
const url = env.SMTP_URL?.trim();
|
|
69
|
+
const host = env.SMTP_HOST?.trim();
|
|
70
|
+
if (!url && !host) {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'error',
|
|
73
|
+
senderKind,
|
|
74
|
+
reason: 'GGUI_EMAIL_SENDER=smtp but neither SMTP_URL nor SMTP_HOST is set',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const portRaw = env.SMTP_PORT?.trim();
|
|
78
|
+
const port = portRaw ? Number(portRaw) : undefined;
|
|
79
|
+
if (portRaw && (Number.isNaN(port) || port <= 0 || port > 65535)) {
|
|
80
|
+
return {
|
|
81
|
+
kind: 'error',
|
|
82
|
+
senderKind,
|
|
83
|
+
reason: `SMTP_PORT='${portRaw}' is not a valid port number`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const secureRaw = env.SMTP_SECURE?.trim().toLowerCase();
|
|
87
|
+
const secure = secureRaw === 'true' ? true : secureRaw === 'false' ? false : undefined;
|
|
88
|
+
try {
|
|
89
|
+
const sender = new SmtpEmailSender({
|
|
90
|
+
...(url ? { url } : {}),
|
|
91
|
+
...(host ? { host } : {}),
|
|
92
|
+
...(port !== undefined ? { port } : {}),
|
|
93
|
+
...(secure !== undefined ? { secure } : {}),
|
|
94
|
+
...(env.SMTP_USER ? { user: env.SMTP_USER } : {}),
|
|
95
|
+
...(env.SMTP_PASS ? { pass: env.SMTP_PASS } : {}),
|
|
96
|
+
...(opts.logger ? { logger: opts.logger } : {}),
|
|
97
|
+
});
|
|
98
|
+
const result = {
|
|
99
|
+
kind: 'ok',
|
|
100
|
+
sender,
|
|
101
|
+
senderKind,
|
|
102
|
+
};
|
|
103
|
+
return fromAddress ? { ...result, fromAddress } : result;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'error',
|
|
108
|
+
senderKind,
|
|
109
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EmailMessage, EmailSender } from './email-login.js';
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface SmtpEmailSenderOptions {
|
|
7
|
+
/**
|
|
8
|
+
* SMTP connection URL, e.g. `smtps://user:pass@host:465` or
|
|
9
|
+
* `smtp://user:pass@host:587`. Convenient for env-driven config.
|
|
10
|
+
* Mutually exclusive with the discrete `host`/`port`/etc. fields.
|
|
11
|
+
*/
|
|
12
|
+
readonly url?: string;
|
|
13
|
+
/** SMTP server hostname. Required if `url` is omitted. */
|
|
14
|
+
readonly host?: string;
|
|
15
|
+
/** SMTP server port. Common: 465 (TLS), 587 (STARTTLS), 25 (plaintext). */
|
|
16
|
+
readonly port?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to use a fully-encrypted TLS socket (port 465). Set
|
|
19
|
+
* `false` for STARTTLS upgrade on 587. Defaults to `port === 465`.
|
|
20
|
+
*/
|
|
21
|
+
readonly secure?: boolean;
|
|
22
|
+
/** SMTP auth username. */
|
|
23
|
+
readonly user?: string;
|
|
24
|
+
/** SMTP auth password. */
|
|
25
|
+
readonly pass?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional logger. Defaults to a component-bound console logger.
|
|
28
|
+
* Emits one `email_smtp_sent` event per successful delivery; SMTP
|
|
29
|
+
* errors propagate via thrown exception (caller logs).
|
|
30
|
+
*/
|
|
31
|
+
readonly logger?: Logger;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export declare class SmtpEmailSender implements EmailSender {
|
|
37
|
+
private readonly transporter;
|
|
38
|
+
private readonly logger;
|
|
39
|
+
constructor(opts: SmtpEmailSenderOptions);
|
|
40
|
+
send(message: EmailMessage): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=email-smtp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-smtp.d.ts","sourceRoot":"","sources":["../src/email-smtp.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,IAAI,EAAE,sBAAsB;IA6BlC,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBjD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP-backed `EmailSender` via nodemailer.
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for `ConsoleEmailSender` — works against any
|
|
5
|
+
* SMTP server: SES SMTP endpoint, Gmail App Password, Postmark SMTP,
|
|
6
|
+
* Mailgun, a self-hosted Postfix, etc. No vendor lock-in.
|
|
7
|
+
*
|
|
8
|
+
* The OSS CLI auto-constructs this when `GGUI_EMAIL_SENDER=smtp` and
|
|
9
|
+
* either `SMTP_URL` or the discrete `SMTP_HOST/SMTP_PORT/SMTP_USER/
|
|
10
|
+
* SMTP_PASS` set are present. Programmatic embedders pass options
|
|
11
|
+
* directly:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { SmtpEmailSender, createGguiServer } from '@ggui-ai/mcp-server';
|
|
15
|
+
*
|
|
16
|
+
* const server = createGguiServer({
|
|
17
|
+
* emailLogin: {
|
|
18
|
+
* sender: new SmtpEmailSender({
|
|
19
|
+
* url: 'smtps://AKIA...:secret@email-smtp.us-east-1.amazonaws.com:465',
|
|
20
|
+
* }),
|
|
21
|
+
* fromAddress: 'Acme <noreply@acme.com>',
|
|
22
|
+
* },
|
|
23
|
+
* // ...
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Either `url` OR `host` (with the rest of the discrete fields) is
|
|
28
|
+
* required. Passing both is a configuration error and throws at
|
|
29
|
+
* construction time.
|
|
30
|
+
*/
|
|
31
|
+
import nodemailer from 'nodemailer';
|
|
32
|
+
import { createConsoleLogger } from './logger.js';
|
|
33
|
+
/**
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export class SmtpEmailSender {
|
|
37
|
+
transporter;
|
|
38
|
+
logger;
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
if (opts.url && opts.host) {
|
|
41
|
+
throw new Error('SmtpEmailSender: pass `url` OR `host` (with port/user/pass), not both.');
|
|
42
|
+
}
|
|
43
|
+
if (!opts.url && !opts.host) {
|
|
44
|
+
throw new Error('SmtpEmailSender: `url` or `host` is required (set SMTP_URL or SMTP_HOST in the environment).');
|
|
45
|
+
}
|
|
46
|
+
this.transporter = opts.url
|
|
47
|
+
? nodemailer.createTransport(opts.url)
|
|
48
|
+
: nodemailer.createTransport({
|
|
49
|
+
host: opts.host,
|
|
50
|
+
...(opts.port !== undefined ? { port: opts.port } : {}),
|
|
51
|
+
...(opts.secure !== undefined
|
|
52
|
+
? { secure: opts.secure }
|
|
53
|
+
: opts.port === 465
|
|
54
|
+
? { secure: true }
|
|
55
|
+
: {}),
|
|
56
|
+
...(opts.user || opts.pass
|
|
57
|
+
? { auth: { user: opts.user ?? '', pass: opts.pass ?? '' } }
|
|
58
|
+
: {}),
|
|
59
|
+
});
|
|
60
|
+
this.logger =
|
|
61
|
+
opts.logger ?? createConsoleLogger({ component: 'email-smtp-sender' });
|
|
62
|
+
}
|
|
63
|
+
async send(message) {
|
|
64
|
+
if (!message.from) {
|
|
65
|
+
throw new Error('SmtpEmailSender: `from` address required (set `emailLogin.fromAddress` on createGguiServer).');
|
|
66
|
+
}
|
|
67
|
+
const info = await this.transporter.sendMail({
|
|
68
|
+
from: message.from,
|
|
69
|
+
to: message.to,
|
|
70
|
+
subject: message.subject,
|
|
71
|
+
text: message.text,
|
|
72
|
+
...(message.html ? { html: message.html } : {}),
|
|
73
|
+
});
|
|
74
|
+
this.logger.info('email_smtp_sent', {
|
|
75
|
+
to: message.to,
|
|
76
|
+
from: message.from,
|
|
77
|
+
messageId: info.messageId,
|
|
78
|
+
response: info.response,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|