@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,221 @@
|
|
|
1
|
+
const ESCAPE_MAP = {
|
|
2
|
+
'&': '&',
|
|
3
|
+
'<': '<',
|
|
4
|
+
'>': '>',
|
|
5
|
+
'"': '"',
|
|
6
|
+
"'": ''',
|
|
7
|
+
};
|
|
8
|
+
const escapeHtml = (raw) => raw.replace(/[&<>"']/g, (ch) => ESCAPE_MAP[ch] ?? ch);
|
|
9
|
+
const operatorBlock = (op) => {
|
|
10
|
+
if (!op)
|
|
11
|
+
return '';
|
|
12
|
+
const { name, url, tagline, contact } = op;
|
|
13
|
+
if (!name && !url && !tagline && !contact)
|
|
14
|
+
return '';
|
|
15
|
+
const heading = name
|
|
16
|
+
? url
|
|
17
|
+
? `<a class="op-link" href="${escapeHtml(url)}" rel="noopener noreferrer">${escapeHtml(name)}</a>`
|
|
18
|
+
: escapeHtml(name)
|
|
19
|
+
: url
|
|
20
|
+
? `<a class="op-link" href="${escapeHtml(url)}" rel="noopener noreferrer">${escapeHtml(url)}</a>`
|
|
21
|
+
: '';
|
|
22
|
+
const taglineLine = tagline
|
|
23
|
+
? `<p class="op-tagline">${escapeHtml(tagline)}</p>`
|
|
24
|
+
: '';
|
|
25
|
+
const contactLine = contact
|
|
26
|
+
? `<p class="op-contact"><a class="cta" href="mailto:${escapeHtml(contact)}">${escapeHtml(contact)}</a></p>`
|
|
27
|
+
: '';
|
|
28
|
+
return `
|
|
29
|
+
<section class="block">
|
|
30
|
+
<p class="eyebrow">Operated by</p>
|
|
31
|
+
${heading ? `<p class="op-name">${heading}</p>` : ''}
|
|
32
|
+
${taglineLine}
|
|
33
|
+
${contactLine}
|
|
34
|
+
</section>`;
|
|
35
|
+
};
|
|
36
|
+
// Inline ggui Wordmark — paper/ink primitives per brand-kit v1.0.
|
|
37
|
+
// Mirrors `apps/landing-ggui-ai/src/components/Wordmark.tsx`. Kept
|
|
38
|
+
// inline (not externally fetched) so the welcome page renders without
|
|
39
|
+
// any network round-trips.
|
|
40
|
+
const WORDMARK_SVG = `<svg viewBox="0 0 224 50" width="112" height="25" aria-label="ggui — generative graphical user interface">
|
|
41
|
+
<path d="M 0 0 H 50 V 25 H 25 V 50 H 0 Z" class="chrome-fill" />
|
|
42
|
+
<rect x="33" y="33" width="17" height="17" class="ink-fill" />
|
|
43
|
+
<path d="M 58 0 H 108 V 25 H 83 V 50 H 58 Z" class="ink-fill" />
|
|
44
|
+
<rect x="91" y="33" width="17" height="17" class="chrome-fill" />
|
|
45
|
+
<path d="M 141 50 C 154.807 50 166 38.8071 166 25 V 0 H 116 V 25 C 116 38.8071 127.193 50 141 50 Z" class="ink-fill" />
|
|
46
|
+
<rect x="174" y="0" width="50" height="50" class="chrome-fill" />
|
|
47
|
+
</svg>`;
|
|
48
|
+
export const renderWelcomeHtml = (inputs, serverName) => {
|
|
49
|
+
const appName = inputs.appName ?? serverName;
|
|
50
|
+
const title = appName ? `${appName} — ggui` : 'ggui';
|
|
51
|
+
return `<!doctype html>
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head>
|
|
54
|
+
<meta charset="utf-8" />
|
|
55
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
56
|
+
<title>${escapeHtml(title)}</title>
|
|
57
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
58
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
59
|
+
<link
|
|
60
|
+
rel="stylesheet"
|
|
61
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist+Mono:wght@400;500;700&display=swap"
|
|
62
|
+
/>
|
|
63
|
+
<style>
|
|
64
|
+
:root {
|
|
65
|
+
color-scheme: light dark;
|
|
66
|
+
--paper: #f4f3ed;
|
|
67
|
+
--paper-2: #ebe9e1;
|
|
68
|
+
--chrome: #d9d9d9;
|
|
69
|
+
--chrome-2: #e4e4e2;
|
|
70
|
+
--ink: #292929;
|
|
71
|
+
--ink-2: #3d3d3d;
|
|
72
|
+
--ink-3: #5a5a5a;
|
|
73
|
+
--ink-4: #8c8c93;
|
|
74
|
+
--line: #d6d4cb;
|
|
75
|
+
--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
76
|
+
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
77
|
+
}
|
|
78
|
+
@media (prefers-color-scheme: dark) {
|
|
79
|
+
:root {
|
|
80
|
+
--paper: #1a1a1a;
|
|
81
|
+
--paper-2: #1f1f1f;
|
|
82
|
+
--chrome: #5a5a5a;
|
|
83
|
+
--chrome-2: #3d3d3d;
|
|
84
|
+
--ink: #f4f3ed;
|
|
85
|
+
--ink-2: #ebe9e1;
|
|
86
|
+
--ink-3: #d9d9d9;
|
|
87
|
+
--ink-4: #8c8c93;
|
|
88
|
+
--line: #3d3d3d;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
* { box-sizing: border-box; }
|
|
92
|
+
html, body {
|
|
93
|
+
margin: 0;
|
|
94
|
+
background: var(--paper);
|
|
95
|
+
color: var(--ink);
|
|
96
|
+
font-family: var(--font-sans);
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
line-height: 1.5;
|
|
99
|
+
font-feature-settings: "ss01", "cv11";
|
|
100
|
+
-webkit-font-smoothing: antialiased;
|
|
101
|
+
}
|
|
102
|
+
a { color: inherit; }
|
|
103
|
+
.chrome-fill { fill: var(--chrome); }
|
|
104
|
+
.ink-fill { fill: var(--ink); }
|
|
105
|
+
main {
|
|
106
|
+
max-width: 560px;
|
|
107
|
+
margin: 0 auto;
|
|
108
|
+
padding: 64px 24px 48px;
|
|
109
|
+
}
|
|
110
|
+
.mark { margin-bottom: 40px; }
|
|
111
|
+
.mark svg { display: block; }
|
|
112
|
+
.eyebrow {
|
|
113
|
+
font-family: var(--font-mono);
|
|
114
|
+
font-size: 11px;
|
|
115
|
+
letter-spacing: 0.18em;
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
color: var(--ink-4);
|
|
118
|
+
margin: 0 0 8px;
|
|
119
|
+
}
|
|
120
|
+
.title {
|
|
121
|
+
font-size: 22px;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
letter-spacing: -0.01em;
|
|
124
|
+
margin: 0 0 4px;
|
|
125
|
+
}
|
|
126
|
+
.subtitle {
|
|
127
|
+
font-family: var(--font-mono);
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
color: var(--ink-3);
|
|
130
|
+
margin: 0;
|
|
131
|
+
}
|
|
132
|
+
.block {
|
|
133
|
+
margin-top: 32px;
|
|
134
|
+
padding-top: 24px;
|
|
135
|
+
border-top: 1px solid var(--line);
|
|
136
|
+
}
|
|
137
|
+
.op-name { font-size: 15px; font-weight: 600; margin: 0 0 4px; }
|
|
138
|
+
.op-tagline { color: var(--ink-3); margin: 0; }
|
|
139
|
+
.op-contact { margin: 12px 0 0; }
|
|
140
|
+
.op-link {
|
|
141
|
+
text-decoration: none;
|
|
142
|
+
border-bottom: 1px solid var(--line);
|
|
143
|
+
padding-bottom: 1px;
|
|
144
|
+
transition: border-color 120ms ease;
|
|
145
|
+
}
|
|
146
|
+
.op-link:hover { border-color: var(--ink); }
|
|
147
|
+
.surfaces { display: flex; flex-direction: column; gap: 12px; }
|
|
148
|
+
.surface {
|
|
149
|
+
padding: 14px 16px;
|
|
150
|
+
background: var(--paper-2);
|
|
151
|
+
border: 1px solid var(--line);
|
|
152
|
+
font-family: var(--font-mono);
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
line-height: 1.6;
|
|
155
|
+
}
|
|
156
|
+
.surface-label {
|
|
157
|
+
display: block;
|
|
158
|
+
font-family: var(--font-sans);
|
|
159
|
+
font-size: 11px;
|
|
160
|
+
letter-spacing: 0.06em;
|
|
161
|
+
text-transform: uppercase;
|
|
162
|
+
color: var(--ink-4);
|
|
163
|
+
margin-bottom: 4px;
|
|
164
|
+
}
|
|
165
|
+
.surface code {
|
|
166
|
+
color: var(--ink);
|
|
167
|
+
font-family: inherit;
|
|
168
|
+
}
|
|
169
|
+
.surface .desc {
|
|
170
|
+
display: block;
|
|
171
|
+
margin-top: 6px;
|
|
172
|
+
font-family: var(--font-sans);
|
|
173
|
+
color: var(--ink-3);
|
|
174
|
+
font-size: 13px;
|
|
175
|
+
}
|
|
176
|
+
.login {
|
|
177
|
+
margin-top: 40px;
|
|
178
|
+
padding-top: 24px;
|
|
179
|
+
border-top: 1px solid var(--line);
|
|
180
|
+
}
|
|
181
|
+
.cta {
|
|
182
|
+
font-family: var(--font-mono);
|
|
183
|
+
font-size: 13px;
|
|
184
|
+
letter-spacing: 0.04em;
|
|
185
|
+
color: var(--ink);
|
|
186
|
+
text-decoration: none;
|
|
187
|
+
border-bottom: 1px solid var(--line);
|
|
188
|
+
padding-bottom: 2px;
|
|
189
|
+
transition: border-color 120ms ease;
|
|
190
|
+
}
|
|
191
|
+
.cta:hover { border-color: var(--ink); }
|
|
192
|
+
</style>
|
|
193
|
+
</head>
|
|
194
|
+
<body>
|
|
195
|
+
<main>
|
|
196
|
+
<div class="mark">${WORDMARK_SVG}</div>
|
|
197
|
+
<p class="eyebrow">App</p>
|
|
198
|
+
<h1 class="title">${escapeHtml(appName || 'ggui')}</h1>
|
|
199
|
+
<p class="subtitle">A ggui server.</p>
|
|
200
|
+
${operatorBlock(inputs.operator)}
|
|
201
|
+
<section class="block">
|
|
202
|
+
<p class="eyebrow">Public surfaces</p>
|
|
203
|
+
<div class="surfaces">
|
|
204
|
+
<div class="surface">
|
|
205
|
+
<span class="surface-label">Session viewer</span>
|
|
206
|
+
<code>/s/<short-code></code>
|
|
207
|
+
<span class="desc">Open the live session your agent linked you to.</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="surface">
|
|
210
|
+
<span class="surface-label">Blueprint preview</span>
|
|
211
|
+
<code>/preview/<id></code>
|
|
212
|
+
<span class="desc">View a published UI blueprint by id.</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</section>
|
|
216
|
+
<p class="login"><a class="cta" href="/admin-login">Operator login →</a></p>
|
|
217
|
+
</main>
|
|
218
|
+
</body>
|
|
219
|
+
</html>
|
|
220
|
+
`;
|
|
221
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF middleware.
|
|
3
|
+
*
|
|
4
|
+
* Double-submit token pattern: token = `${randomB64url}.${hmacB64url}`,
|
|
5
|
+
* HMAC-SHA256 over `${random}|${sessionBearer ?? ''}` with the
|
|
6
|
+
* server-provided secret. Binding the HMAC to the current session
|
|
7
|
+
* bearer prevents an anonymous-session token from being replayed
|
|
8
|
+
* after the user logs in (post-login the cookie value differs, so
|
|
9
|
+
* the recomputed HMAC won't match).
|
|
10
|
+
*
|
|
11
|
+
* The middleware skips:
|
|
12
|
+
*
|
|
13
|
+
* - Safe HTTP methods (GET / HEAD / OPTIONS).
|
|
14
|
+
* - Operator-configured `skipPaths` (default: `/pair`,
|
|
15
|
+
* `/ggui/email-login/start`) — pre-auth endpoints that exist
|
|
16
|
+
* BEFORE the user has a session.
|
|
17
|
+
* - Programmatic Bearer requests with no session cookie. CSRF is a
|
|
18
|
+
* browser-context defense; the claude.ai connector posts pre-auth
|
|
19
|
+
* bearers without a same-origin cookie context.
|
|
20
|
+
*
|
|
21
|
+
* On every GET response that has a session cookie, the middleware
|
|
22
|
+
* also sets `X-Ggui-CSRF-Token` so SPA bootstrap reads the latest
|
|
23
|
+
* token straight off the response without an extra round-trip.
|
|
24
|
+
* Anonymous GETs get a token bound to empty-bearer for pre-auth
|
|
25
|
+
* POSTs (e.g. magic-link start).
|
|
26
|
+
*/
|
|
27
|
+
import type { Express, RequestHandler } from 'express';
|
|
28
|
+
import type { Logger } from './logger.js';
|
|
29
|
+
export declare const CSRF_HEADER_NAME = "X-Ggui-CSRF";
|
|
30
|
+
export declare const CSRF_RESPONSE_HEADER_NAME = "X-Ggui-CSRF-Token";
|
|
31
|
+
export declare const DEFAULT_CSRF_TOKEN_PATH = "/ggui/csrf-token";
|
|
32
|
+
export interface CsrfMiddlewareOptions {
|
|
33
|
+
readonly secret: string;
|
|
34
|
+
readonly logger: Logger;
|
|
35
|
+
readonly skipPaths?: ReadonlyArray<string>;
|
|
36
|
+
}
|
|
37
|
+
export interface MintCsrfTokenInput {
|
|
38
|
+
readonly sessionBearer: string | null;
|
|
39
|
+
readonly secret: string;
|
|
40
|
+
}
|
|
41
|
+
/** Mint a fresh CSRF token bound to `sessionBearer`. Format:
|
|
42
|
+
* `${randomBase64url}.${hmacBase64url}`. */
|
|
43
|
+
export declare function mintCsrfToken(input: MintCsrfTokenInput): string;
|
|
44
|
+
/** Express middleware: enforces CSRF on state-changing requests + sets
|
|
45
|
+
* `X-Ggui-CSRF-Token` on safe responses. */
|
|
46
|
+
export declare function createCsrfMiddleware(opts: CsrfMiddlewareOptions): RequestHandler;
|
|
47
|
+
export interface MountCsrfTokenRouteOptions {
|
|
48
|
+
readonly secret: string;
|
|
49
|
+
readonly path?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Mount `GET /ggui/csrf-token` returning `{ token, expiresAt }`. SPAs
|
|
52
|
+
* call this on app boot when they prefer an explicit fetch over
|
|
53
|
+
* reading `X-Ggui-CSRF-Token` off another response. */
|
|
54
|
+
export declare function mountCsrfTokenRoute(app: Express, opts: MountCsrfTokenRouteOptions): void;
|
|
55
|
+
//# sourceMappingURL=csrf-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf-middleware.d.ts","sourceRoot":"","sources":["../src/csrf-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAW,cAAc,EAAY,MAAM,SAAS,CAAC;AAG1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAC9C,eAAO,MAAM,yBAAyB,sBAAsB,CAAC;AAC7D,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAc1D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAiBD;6CAC6C;AAC7C,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAI/D;AAmCD;6CAC6C;AAC7C,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CAiEhF;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;wDAEwD;AACxD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,0BAA0B,GAC/B,IAAI,CAUN"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { readUserSessionCookie } from './user-session-auth.js';
|
|
3
|
+
export const CSRF_HEADER_NAME = 'X-Ggui-CSRF';
|
|
4
|
+
export const CSRF_RESPONSE_HEADER_NAME = 'X-Ggui-CSRF-Token';
|
|
5
|
+
export const DEFAULT_CSRF_TOKEN_PATH = '/ggui/csrf-token';
|
|
6
|
+
// Anonymous flow-starters — no authenticated-state mutation, so CSRF
|
|
7
|
+
// adds no protection. Same threat model as `/pair` (rate-limited,
|
|
8
|
+
// idempotent, the magic link goes to the typed email not the
|
|
9
|
+
// attacker). Verify is a GET and auto-skipped via SAFE_METHODS.
|
|
10
|
+
const DEFAULT_SKIP_PATHS = [
|
|
11
|
+
'/pair',
|
|
12
|
+
'/ggui/email-login/start',
|
|
13
|
+
];
|
|
14
|
+
const RANDOM_BYTES = 16;
|
|
15
|
+
/** Header-side TTL hint (5 min). The server doesn't cache tokens; the
|
|
16
|
+
* HMAC itself binds validity to the current session bearer. */
|
|
17
|
+
const TOKEN_LIFETIME_MS = 5 * 60 * 1000;
|
|
18
|
+
function base64url(bytes) {
|
|
19
|
+
return bytes
|
|
20
|
+
.toString('base64')
|
|
21
|
+
.replace(/=+$/g, '')
|
|
22
|
+
.replace(/\+/g, '-')
|
|
23
|
+
.replace(/\//g, '_');
|
|
24
|
+
}
|
|
25
|
+
function computeHmac(random, sessionBearer, secret) {
|
|
26
|
+
const mac = createHmac('sha256', secret)
|
|
27
|
+
.update(`${random}|${sessionBearer ?? ''}`)
|
|
28
|
+
.digest();
|
|
29
|
+
return base64url(mac);
|
|
30
|
+
}
|
|
31
|
+
/** Mint a fresh CSRF token bound to `sessionBearer`. Format:
|
|
32
|
+
* `${randomBase64url}.${hmacBase64url}`. */
|
|
33
|
+
export function mintCsrfToken(input) {
|
|
34
|
+
const random = base64url(randomBytes(RANDOM_BYTES));
|
|
35
|
+
const sig = computeHmac(random, input.sessionBearer, input.secret);
|
|
36
|
+
return `${random}.${sig}`;
|
|
37
|
+
}
|
|
38
|
+
function validateCsrfToken(token, sessionBearer, secret) {
|
|
39
|
+
const parts = token.split('.');
|
|
40
|
+
if (parts.length !== 2)
|
|
41
|
+
return false;
|
|
42
|
+
const [random, sig] = parts;
|
|
43
|
+
if (!random || !sig)
|
|
44
|
+
return false;
|
|
45
|
+
const expected = computeHmac(random, sessionBearer, secret);
|
|
46
|
+
// timingSafeEqual requires equal-length buffers; cheap length check
|
|
47
|
+
// first to avoid throwing on malformed input.
|
|
48
|
+
if (expected.length !== sig.length)
|
|
49
|
+
return false;
|
|
50
|
+
return timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(sig, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
function isSafeMethod(method) {
|
|
53
|
+
return method === 'GET' || method === 'HEAD' || method === 'OPTIONS';
|
|
54
|
+
}
|
|
55
|
+
function pathMatches(path, skipPaths) {
|
|
56
|
+
for (const skip of skipPaths) {
|
|
57
|
+
if (path === skip)
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/** Express middleware: enforces CSRF on state-changing requests + sets
|
|
63
|
+
* `X-Ggui-CSRF-Token` on safe responses. */
|
|
64
|
+
export function createCsrfMiddleware(opts) {
|
|
65
|
+
const skipPaths = opts.skipPaths ?? DEFAULT_SKIP_PATHS;
|
|
66
|
+
const reqLogger = opts.logger.child({ middleware: 'csrf' });
|
|
67
|
+
return (req, res, next) => {
|
|
68
|
+
const sessionBearer = readUserSessionCookie(req);
|
|
69
|
+
// On safe-method responses, set the response header so SPAs read
|
|
70
|
+
// the freshly-minted token without an extra round-trip.
|
|
71
|
+
if (isSafeMethod(req.method)) {
|
|
72
|
+
const token = mintCsrfToken({ sessionBearer, secret: opts.secret });
|
|
73
|
+
res.setHeader(CSRF_RESPONSE_HEADER_NAME, token);
|
|
74
|
+
next();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (pathMatches(req.path, skipPaths)) {
|
|
78
|
+
next();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// CSRF defends ONLY browser-cookie sessions. If there's no
|
|
82
|
+
// user-session cookie on the request, the auth surface is one
|
|
83
|
+
// of: (a) programmatic Bearer (claude.ai connector) — no
|
|
84
|
+
// cross-origin cookie to ride; (b) fully unauthenticated — the
|
|
85
|
+
// downstream auth gate will 401. Skip CSRF in both cases so
|
|
86
|
+
// CSRF doesn't masquerade as the auth gate (which produces
|
|
87
|
+
// misleading 403s on 401-deserving requests).
|
|
88
|
+
if (sessionBearer === null) {
|
|
89
|
+
// No cookie: cookieAuthMiddleware may have synthesized
|
|
90
|
+
// `Authorization: Bearer <cookie>` upstream, but we read the
|
|
91
|
+
// cookie directly above so the synthesis doesn't confuse the
|
|
92
|
+
// skip rule. Bearer-without-cookie programmatic requests
|
|
93
|
+
// (claude.ai connector) and fully unauthenticated requests
|
|
94
|
+
// both fall through here.
|
|
95
|
+
next();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const headerValueRaw = req.headers[CSRF_HEADER_NAME.toLowerCase()];
|
|
99
|
+
const headerValue = Array.isArray(headerValueRaw)
|
|
100
|
+
? headerValueRaw[0]
|
|
101
|
+
: headerValueRaw;
|
|
102
|
+
if (typeof headerValue !== 'string' || headerValue.length === 0) {
|
|
103
|
+
reqLogger.warn('csrf_missing', { path: req.path, method: req.method });
|
|
104
|
+
res.status(403).json({
|
|
105
|
+
error: {
|
|
106
|
+
code: 'csrf_required',
|
|
107
|
+
message: 'Missing or invalid CSRF token.',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!validateCsrfToken(headerValue, sessionBearer, opts.secret)) {
|
|
113
|
+
reqLogger.warn('csrf_mismatch', { path: req.path, method: req.method });
|
|
114
|
+
res.status(403).json({
|
|
115
|
+
error: {
|
|
116
|
+
code: 'csrf_required',
|
|
117
|
+
message: 'Missing or invalid CSRF token.',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
next();
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Mount `GET /ggui/csrf-token` returning `{ token, expiresAt }`. SPAs
|
|
126
|
+
* call this on app boot when they prefer an explicit fetch over
|
|
127
|
+
* reading `X-Ggui-CSRF-Token` off another response. */
|
|
128
|
+
export function mountCsrfTokenRoute(app, opts) {
|
|
129
|
+
const path = opts.path ?? DEFAULT_CSRF_TOKEN_PATH;
|
|
130
|
+
app.get(path, (req, res) => {
|
|
131
|
+
const sessionBearer = readUserSessionCookie(req);
|
|
132
|
+
const token = mintCsrfToken({ sessionBearer, secret: opts.secret });
|
|
133
|
+
res.status(200).json({
|
|
134
|
+
token,
|
|
135
|
+
expiresAt: Date.now() + TOKEN_LIFETIME_MS,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email magic-link login.
|
|
3
|
+
*
|
|
4
|
+
* Passwordless flow: user enters their email → server mints a single-
|
|
5
|
+
* use token → server emails the user a `verify` URL → user clicks
|
|
6
|
+
* → server consumes the token + mints a session bearer → 302 to
|
|
7
|
+
* `nextPath`. Two routes (`/start`, `/verify`) plus a public-readable
|
|
8
|
+
* `/config` so `/login` can know whether to render the form.
|
|
9
|
+
*
|
|
10
|
+
* Identity model: `userId = "email:" + normalizedEmail`. Different
|
|
11
|
+
* email = different identity (no email-rotation linking, same
|
|
12
|
+
* stance as oauth-login-types.ts). Lowercase + trim is the only
|
|
13
|
+
* normalization — Gmail-dot/plus-tag tricks deliberately stay
|
|
14
|
+
* unmodified so `alice+ggui@gmail.com` is a distinct identity from
|
|
15
|
+
* `alice@gmail.com` (the user is in control of which inbox the
|
|
16
|
+
* link goes to).
|
|
17
|
+
*
|
|
18
|
+
* Security boundary:
|
|
19
|
+
*
|
|
20
|
+
* - Tokens are 256-bit URL-safe, single-use, ≤15 min TTL.
|
|
21
|
+
* Consume-or-reject-atomically per request — a successful verify
|
|
22
|
+
* deletes the token before the response goes out so a refresh
|
|
23
|
+
* can't replay.
|
|
24
|
+
* - `/start` ALWAYS returns 200, regardless of whether the email
|
|
25
|
+
* was deliverable or even valid. This prevents email-enumeration
|
|
26
|
+
* attacks (an attacker probing for "is alice@example.com a known
|
|
27
|
+
* user" would otherwise see a different status / latency).
|
|
28
|
+
* - The verify URL goes ONLY to the user's inbox — never echoed
|
|
29
|
+
* back to the start-call response. The server logs it once for
|
|
30
|
+
* audit, but the user picks it up out-of-band.
|
|
31
|
+
* - `nextPath` is sanitized to same-origin relative paths. An
|
|
32
|
+
* attacker can't craft a magic link that bounces a verified
|
|
33
|
+
* session through to `https://evil/`.
|
|
34
|
+
*/
|
|
35
|
+
import type { Express } from 'express';
|
|
36
|
+
import type { AuditSink, AuthAdapter } from '@ggui-ai/mcp-server-core';
|
|
37
|
+
import type { Logger } from './logger.js';
|
|
38
|
+
/**
|
|
39
|
+
* Public route paths. Operators may override via
|
|
40
|
+
* `EmailLoginRoutesOptions.{startPath,verifyPath,configPath}` for
|
|
41
|
+
* test or sub-mount scenarios.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_EMAIL_LOGIN_START_PATH = "/ggui/email-login/start";
|
|
44
|
+
export declare const DEFAULT_EMAIL_LOGIN_VERIFY_PATH = "/ggui/email-login/verify";
|
|
45
|
+
export declare const DEFAULT_EMAIL_LOGIN_CONFIG_PATH = "/ggui/email-login/config";
|
|
46
|
+
/**
|
|
47
|
+
* Outbound email contract. The default `ConsoleEmailSender` just
|
|
48
|
+
* logs to the server logger — useful for `ggui serve` development
|
|
49
|
+
* (the magic link shows up in the terminal). Reference
|
|
50
|
+
* implementations (SMTP via nodemailer, Resend, AWS SES) live in
|
|
51
|
+
* adapter packages so the OSS core stays dep-light.
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export interface EmailSender {
|
|
56
|
+
send(message: EmailMessage): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Plain-text first, HTML second — every transactional-email
|
|
60
|
+
* service we'd plausibly target accepts this shape. `from` lets the
|
|
61
|
+
* caller override the default `fromAddress` per message; absent
|
|
62
|
+
* means the server-wide default.
|
|
63
|
+
*
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
export interface EmailMessage {
|
|
67
|
+
readonly to: string;
|
|
68
|
+
readonly from?: string;
|
|
69
|
+
readonly subject: string;
|
|
70
|
+
readonly text: string;
|
|
71
|
+
readonly html?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Fallback sender that logs every "sent" email to the configured
|
|
75
|
+
* logger at info level. The verify URL appears in the logs so an
|
|
76
|
+
* operator running `ggui serve` locally can copy/paste it without
|
|
77
|
+
* setting up real email infrastructure. Production deploys MUST
|
|
78
|
+
* swap this for a real sender — the fallback is a developer-mode
|
|
79
|
+
* convenience, not a security control.
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
export declare class ConsoleEmailSender implements EmailSender {
|
|
84
|
+
private readonly logger;
|
|
85
|
+
constructor(logger?: Logger);
|
|
86
|
+
send(message: EmailMessage): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Storage for outstanding magic-link tokens. Each `mintToken` returns
|
|
90
|
+
* an opaque URL-safe string the caller emails to the user; the user's
|
|
91
|
+
* subsequent click hits `/verify`, which calls `consumeToken` (atomic
|
|
92
|
+
* single-use). The default `InMemoryMagicLinkStore` is per-process
|
|
93
|
+
* and forgets on restart — fine for a single-host OSS server, wrong
|
|
94
|
+
* for multi-host. Cluster deployments swap in a Redis/DDB-backed
|
|
95
|
+
* impl whose contract matches this interface.
|
|
96
|
+
*
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
export interface MagicLinkStore {
|
|
100
|
+
mintToken(input: MintTokenInput): Promise<string>;
|
|
101
|
+
/**
|
|
102
|
+
* Atomic consume — returns `null` if the token is unknown,
|
|
103
|
+
* already consumed, or expired. Successful consumption MUST
|
|
104
|
+
* delete the token before returning so a replay finds nothing.
|
|
105
|
+
*/
|
|
106
|
+
consumeToken(token: string): Promise<MagicLinkRecord | null>;
|
|
107
|
+
}
|
|
108
|
+
export interface MintTokenInput {
|
|
109
|
+
readonly email: string;
|
|
110
|
+
readonly nextPath: string;
|
|
111
|
+
readonly ttlMs: number;
|
|
112
|
+
}
|
|
113
|
+
export interface MagicLinkRecord {
|
|
114
|
+
readonly email: string;
|
|
115
|
+
readonly nextPath: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Default in-memory store. Loses tokens on process restart (which
|
|
119
|
+
* is acceptable — pending magic links expire in 15 min anyway, and
|
|
120
|
+
* a restart is itself a "request a new link" prompt to the user).
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
export declare class InMemoryMagicLinkStore implements MagicLinkStore {
|
|
125
|
+
private readonly records;
|
|
126
|
+
mintToken({ email, nextPath, ttlMs }: MintTokenInput): Promise<string>;
|
|
127
|
+
consumeToken(token: string): Promise<MagicLinkRecord | null>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Configuration for {@link mountEmailLoginRoutes}.
|
|
131
|
+
*
|
|
132
|
+
* @public
|
|
133
|
+
*/
|
|
134
|
+
export interface EmailLoginRoutesOptions {
|
|
135
|
+
readonly sender: EmailSender;
|
|
136
|
+
readonly auth: AuthAdapter;
|
|
137
|
+
readonly logger: Logger;
|
|
138
|
+
/** Public base URL — used to compose the magic-link verify URL. */
|
|
139
|
+
readonly publicBaseUrl: string;
|
|
140
|
+
/**
|
|
141
|
+
* From-address stamped on every email. Format follows RFC 5322:
|
|
142
|
+
* `"display name" <addr@host>` or just `addr@host`. SMTP/Resend
|
|
143
|
+
* typically REQUIRE the address to be on a domain the sender has
|
|
144
|
+
* authenticated; check the sender adapter's docs.
|
|
145
|
+
*/
|
|
146
|
+
readonly fromAddress: string;
|
|
147
|
+
/** Override store (e.g., RedisMagicLinkStore for multi-host). */
|
|
148
|
+
readonly store?: MagicLinkStore;
|
|
149
|
+
/** Optional audit sink — fires `auth.email.start/.success/.failure`. */
|
|
150
|
+
readonly auditSink?: AuditSink;
|
|
151
|
+
/** Adds `Secure` to the session cookie. */
|
|
152
|
+
readonly secure?: boolean;
|
|
153
|
+
/** User-session cookie TTL (seconds). */
|
|
154
|
+
readonly ttlSec?: number;
|
|
155
|
+
/** Path overrides (mostly for tests). */
|
|
156
|
+
readonly startPath?: string;
|
|
157
|
+
readonly verifyPath?: string;
|
|
158
|
+
readonly configPath?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Subject line + body templating. Defaults are sensible; override
|
|
161
|
+
* to match brand copy. `{verifyUrl}` is replaced before send.
|
|
162
|
+
*/
|
|
163
|
+
readonly subject?: string;
|
|
164
|
+
readonly bodyText?: string;
|
|
165
|
+
readonly bodyHtml?: string;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Mount `POST /start`, `GET /verify`, `GET /config`. Idempotent
|
|
169
|
+
* across multiple calls (later mount wins for the same path).
|
|
170
|
+
*
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
export declare function mountEmailLoginRoutes(app: Express, opts: EmailLoginRoutesOptions): void;
|
|
174
|
+
//# sourceMappingURL=email-login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-login.d.ts","sourceRoot":"","sources":["../src/email-login.ts"],"names":[],"mappings":"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"}
|