@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.
Files changed (141) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/admin-blueprints-transport.d.ts +114 -0
  4. package/dist/admin-blueprints-transport.d.ts.map +1 -0
  5. package/dist/admin-blueprints-transport.js +118 -0
  6. package/dist/admin-oauth-providers-transport.d.ts +40 -0
  7. package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
  8. package/dist/admin-oauth-providers-transport.js +263 -0
  9. package/dist/auth.d.ts +39 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +75 -0
  12. package/dist/build-mcp.d.ts +128 -0
  13. package/dist/build-mcp.d.ts.map +1 -0
  14. package/dist/build-mcp.js +113 -0
  15. package/dist/code-store-fs.d.ts +19 -0
  16. package/dist/code-store-fs.d.ts.map +1 -0
  17. package/dist/code-store-fs.js +98 -0
  18. package/dist/console-auth.d.ts +139 -0
  19. package/dist/console-auth.d.ts.map +1 -0
  20. package/dist/console-auth.js +102 -0
  21. package/dist/console-cache.d.ts +78 -0
  22. package/dist/console-cache.d.ts.map +1 -0
  23. package/dist/console-cache.js +105 -0
  24. package/dist/console-headers.d.ts +124 -0
  25. package/dist/console-headers.d.ts.map +1 -0
  26. package/dist/console-headers.js +49 -0
  27. package/dist/console-llm-trace.d.ts +66 -0
  28. package/dist/console-llm-trace.d.ts.map +1 -0
  29. package/dist/console-llm-trace.js +105 -0
  30. package/dist/console-payloads.d.ts +67 -0
  31. package/dist/console-payloads.d.ts.map +1 -0
  32. package/dist/console-payloads.js +105 -0
  33. package/dist/console-theme-routes.d.ts +111 -0
  34. package/dist/console-theme-routes.d.ts.map +1 -0
  35. package/dist/console-theme-routes.js +202 -0
  36. package/dist/console-timeline.d.ts +45 -0
  37. package/dist/console-timeline.d.ts.map +1 -0
  38. package/dist/console-timeline.js +169 -0
  39. package/dist/console-validator.d.ts +67 -0
  40. package/dist/console-validator.d.ts.map +1 -0
  41. package/dist/console-validator.js +105 -0
  42. package/dist/console-welcome.d.ts +7 -0
  43. package/dist/console-welcome.d.ts.map +1 -0
  44. package/dist/console-welcome.js +221 -0
  45. package/dist/csrf-middleware.d.ts +55 -0
  46. package/dist/csrf-middleware.d.ts.map +1 -0
  47. package/dist/csrf-middleware.js +138 -0
  48. package/dist/email-login.d.ts +174 -0
  49. package/dist/email-login.d.ts.map +1 -0
  50. package/dist/email-login.js +254 -0
  51. package/dist/email-resend.d.ts +29 -0
  52. package/dist/email-resend.d.ts.map +1 -0
  53. package/dist/email-resend.js +71 -0
  54. package/dist/email-sender-from-env.d.ts +34 -0
  55. package/dist/email-sender-from-env.d.ts.map +1 -0
  56. package/dist/email-sender-from-env.js +112 -0
  57. package/dist/email-smtp.d.ts +42 -0
  58. package/dist/email-smtp.d.ts.map +1 -0
  59. package/dist/email-smtp.js +81 -0
  60. package/dist/index.d.ts +102 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +122 -0
  63. package/dist/instructions-presets.d.ts +112 -0
  64. package/dist/instructions-presets.d.ts.map +1 -0
  65. package/dist/instructions-presets.js +195 -0
  66. package/dist/llm-backed-negotiator.d.ts +178 -0
  67. package/dist/llm-backed-negotiator.d.ts.map +1 -0
  68. package/dist/llm-backed-negotiator.js +579 -0
  69. package/dist/logger.d.ts +23 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +41 -0
  72. package/dist/mcp-apps-inbound.d.ts +86 -0
  73. package/dist/mcp-apps-inbound.d.ts.map +1 -0
  74. package/dist/mcp-apps-inbound.js +278 -0
  75. package/dist/mcp-apps-outbound.d.ts +448 -0
  76. package/dist/mcp-apps-outbound.d.ts.map +1 -0
  77. package/dist/mcp-apps-outbound.js +1163 -0
  78. package/dist/mcp-mounts.d.ts +239 -0
  79. package/dist/mcp-mounts.d.ts.map +1 -0
  80. package/dist/mcp-mounts.js +222 -0
  81. package/dist/oauth-login-types.d.ts +160 -0
  82. package/dist/oauth-login-types.d.ts.map +1 -0
  83. package/dist/oauth-login-types.js +9 -0
  84. package/dist/oauth-login.d.ts +77 -0
  85. package/dist/oauth-login.d.ts.map +1 -0
  86. package/dist/oauth-login.js +455 -0
  87. package/dist/oauth-providers/github.d.ts +17 -0
  88. package/dist/oauth-providers/github.d.ts.map +1 -0
  89. package/dist/oauth-providers/github.js +89 -0
  90. package/dist/oauth-providers/google.d.ts +18 -0
  91. package/dist/oauth-providers/google.d.ts.map +1 -0
  92. package/dist/oauth-providers/google.js +59 -0
  93. package/dist/oauth-providers-store.d.ts +32 -0
  94. package/dist/oauth-providers-store.d.ts.map +1 -0
  95. package/dist/oauth-providers-store.js +291 -0
  96. package/dist/oauth.d.ts +347 -0
  97. package/dist/oauth.d.ts.map +1 -0
  98. package/dist/oauth.js +686 -0
  99. package/dist/pairing-transport.d.ts +99 -0
  100. package/dist/pairing-transport.d.ts.map +1 -0
  101. package/dist/pairing-transport.js +223 -0
  102. package/dist/rate-limit-middleware.d.ts +36 -0
  103. package/dist/rate-limit-middleware.d.ts.map +1 -0
  104. package/dist/rate-limit-middleware.js +57 -0
  105. package/dist/render-gate.d.ts +87 -0
  106. package/dist/render-gate.d.ts.map +1 -0
  107. package/dist/render-gate.js +77 -0
  108. package/dist/render-rate-limit.d.ts +59 -0
  109. package/dist/render-rate-limit.d.ts.map +1 -0
  110. package/dist/render-rate-limit.js +73 -0
  111. package/dist/render-signing.d.ts +98 -0
  112. package/dist/render-signing.d.ts.map +1 -0
  113. package/dist/render-signing.js +113 -0
  114. package/dist/request-context.d.ts +113 -0
  115. package/dist/request-context.d.ts.map +1 -0
  116. package/dist/request-context.js +154 -0
  117. package/dist/reserved-validators.d.ts +22 -0
  118. package/dist/reserved-validators.d.ts.map +1 -0
  119. package/dist/reserved-validators.js +101 -0
  120. package/dist/schema-compat.d.ts +167 -0
  121. package/dist/schema-compat.d.ts.map +1 -0
  122. package/dist/schema-compat.js +187 -0
  123. package/dist/security-headers-middleware.d.ts +38 -0
  124. package/dist/security-headers-middleware.d.ts.map +1 -0
  125. package/dist/security-headers-middleware.js +30 -0
  126. package/dist/server.d.ts +2060 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +6338 -0
  129. package/dist/session-channel.d.ts +651 -0
  130. package/dist/session-channel.d.ts.map +1 -0
  131. package/dist/session-channel.js +1756 -0
  132. package/dist/storage.d.ts +89 -0
  133. package/dist/storage.d.ts.map +1 -0
  134. package/dist/storage.js +171 -0
  135. package/dist/thread-transport.d.ts +118 -0
  136. package/dist/thread-transport.d.ts.map +1 -0
  137. package/dist/thread-transport.js +478 -0
  138. package/dist/user-session-auth.d.ts +167 -0
  139. package/dist/user-session-auth.d.ts.map +1 -0
  140. package/dist/user-session-auth.js +148 -0
  141. 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
+ }