@andespindola/brainlink 1.6.15 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/application/frontend/auth-styles.js +180 -0
- package/dist/application/frontend/client/account-menu.js +25 -16
- package/dist/application/frontend/client/graph-renderer.js +20 -17
- package/dist/application/frontend/client/rendering.js +5 -5
- package/dist/application/frontend/client/scope-theme.js +23 -18
- package/dist/application/frontend/client-css.js +56 -36
- package/dist/application/frontend/connect-page.js +436 -0
- package/dist/application/frontend/login-page.js +9 -163
- package/dist/application/frontend/neon-theme.js +110 -0
- package/dist/application/frontend/settings-page.js +76 -52
- package/dist/application/server/routes.js +30 -0
- package/dist/mcp/oauth-authorize-page.js +11 -130
- package/package.json +1 -1
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// Self-contained branded "Connect your agent" page for the graph web server.
|
|
2
|
+
// Served as a plain string (inline <style> + inline <script>, no external assets
|
|
3
|
+
// beyond the shared Google Fonts @import, no framework) so it works identically
|
|
4
|
+
// for a global npm install. The visual identity is the unified neon system
|
|
5
|
+
// shared with the auth pages, the settings page and the graph client (see
|
|
6
|
+
// neon-theme.ts): #0a0e1a base, faint cyber grid, cyan #22d3ee + violet #8b5cf6,
|
|
7
|
+
// Space Grotesk headings / DM Sans body, glass card + gradient wordmark.
|
|
8
|
+
//
|
|
9
|
+
// The page renders ready-to-paste MCP setup so a graph owner can wire their
|
|
10
|
+
// agents (Codex, Claude Code, ChatGPT/MCP) to this brainlink. It has two modes,
|
|
11
|
+
// picked server-side:
|
|
12
|
+
// - remote: the server is exposed AND an MCP token is configured. Snippets
|
|
13
|
+
// point at the request host over HTTPS and carry a bearer token.
|
|
14
|
+
// - local: no token / not exposed. Snippets run the local stdio MCP server
|
|
15
|
+
// against the server's configured vault path — never a token.
|
|
16
|
+
// This page is only ever served behind the same session gate as /settings, so
|
|
17
|
+
// the token is shown only to the authenticated graph owner.
|
|
18
|
+
import { neonAmbientEffects, neonFontImport, neonGridBackground, neonRootTokens, neonWordmark } from './neon-theme.js';
|
|
19
|
+
// HTML-escape for text interpolated into the served document. Snippets are
|
|
20
|
+
// rendered as textContent client-side, but the AGENTS.md / labels are inlined,
|
|
21
|
+
// so escape defensively to keep the page well-formed and injection-free.
|
|
22
|
+
const escapeHtml = (value) => value
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"');
|
|
27
|
+
// The compact operating contract shown on the page and offered as a ready
|
|
28
|
+
// AGENTS.md. Neutral/professional, referencing only the agent product names.
|
|
29
|
+
export const connectAgentsMarkdown = () => `# Brainlink — canonical memory
|
|
30
|
+
|
|
31
|
+
Brainlink is your canonical, persistent memory across turns and sessions. Treat
|
|
32
|
+
it as the continuity of your cognition, not an optional lookup.
|
|
33
|
+
|
|
34
|
+
## At session start
|
|
35
|
+
- Run \`brainlink_bootstrap\` with a short \`query\` describing the task. Re-run it
|
|
36
|
+
whenever your context is stale.
|
|
37
|
+
|
|
38
|
+
## Before you act
|
|
39
|
+
- Consult Brainlink before any decision, recommendation, question, or
|
|
40
|
+
implementation. Never act from memory alone when durable context may exist.
|
|
41
|
+
- Retrieve with \`strategy: "auto"\` (CAG on a fresh pack, RAG otherwise) and
|
|
42
|
+
\`mode: "hybrid"\` (FTS + semantic) unless a narrower mode clearly fits.
|
|
43
|
+
|
|
44
|
+
## After you learn
|
|
45
|
+
- Write durable learnings back with \`brainlink_remember\` (or
|
|
46
|
+
\`brainlink_add_note\` for structured notes), then reindex with
|
|
47
|
+
\`brainlink_index\`.
|
|
48
|
+
- Write connected memory: include \`[[wiki links]]\`, tags, and priority markers
|
|
49
|
+
so notes join the knowledge graph instead of sitting orphaned.
|
|
50
|
+
`;
|
|
51
|
+
const connectStyles = () => `${neonFontImport()}
|
|
52
|
+
:root {
|
|
53
|
+
${neonRootTokens()}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
* {
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
html,
|
|
61
|
+
body {
|
|
62
|
+
width: 100%;
|
|
63
|
+
min-height: 100%;
|
|
64
|
+
margin: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
body {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: flex-start;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
min-height: 100vh;
|
|
72
|
+
min-height: 100dvh;
|
|
73
|
+
padding: 40px 24px;
|
|
74
|
+
color: var(--text);
|
|
75
|
+
font-family: var(--font-body);
|
|
76
|
+
${neonGridBackground()}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
${neonAmbientEffects()}
|
|
80
|
+
|
|
81
|
+
button {
|
|
82
|
+
font: inherit;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:focus-visible {
|
|
86
|
+
outline: 2px solid var(--accent);
|
|
87
|
+
outline-offset: 2px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.connect-card {
|
|
91
|
+
width: 100%;
|
|
92
|
+
max-width: 720px;
|
|
93
|
+
padding: 32px 28px;
|
|
94
|
+
border: 1px solid var(--line);
|
|
95
|
+
border-radius: 16px;
|
|
96
|
+
background: var(--panel);
|
|
97
|
+
box-shadow: 0 24px 80px rgba(2, 6, 18, 0.55);
|
|
98
|
+
backdrop-filter: blur(16px);
|
|
99
|
+
animation: neon-fade-up 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@media (prefers-reduced-motion: reduce) {
|
|
103
|
+
.connect-card {
|
|
104
|
+
animation: none;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.connect-brand {
|
|
109
|
+
display: grid;
|
|
110
|
+
gap: 4px;
|
|
111
|
+
margin-bottom: 20px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.connect-brand strong {
|
|
115
|
+
font-family: var(--font-heading);
|
|
116
|
+
font-size: 28px;
|
|
117
|
+
font-weight: 700;
|
|
118
|
+
letter-spacing: -0.01em;
|
|
119
|
+
${neonWordmark()}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.connect-brand span {
|
|
123
|
+
color: var(--muted);
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.connect-lede {
|
|
128
|
+
margin: 0 0 8px;
|
|
129
|
+
color: var(--text);
|
|
130
|
+
font-size: 14px;
|
|
131
|
+
line-height: 1.5;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.connect-mode {
|
|
135
|
+
display: inline-flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
margin-bottom: 20px;
|
|
139
|
+
padding: 6px 12px;
|
|
140
|
+
border: 1px solid var(--line);
|
|
141
|
+
border-radius: 999px;
|
|
142
|
+
background: rgba(19, 26, 42, 0.55);
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.connect-mode strong {
|
|
148
|
+
color: var(--accent);
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.connect-endpoint {
|
|
153
|
+
display: grid;
|
|
154
|
+
gap: 6px;
|
|
155
|
+
margin-bottom: 24px;
|
|
156
|
+
padding: 12px 14px;
|
|
157
|
+
border: 1px solid var(--line);
|
|
158
|
+
border-radius: 10px;
|
|
159
|
+
background: rgba(19, 26, 42, 0.55);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.connect-endpoint span {
|
|
163
|
+
color: var(--muted);
|
|
164
|
+
font-size: 11px;
|
|
165
|
+
letter-spacing: 0.02em;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.connect-endpoint code {
|
|
169
|
+
font-family: var(--font-mono);
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
color: var(--accent);
|
|
172
|
+
overflow-wrap: anywhere;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.connect-section {
|
|
176
|
+
margin-top: 26px;
|
|
177
|
+
padding-top: 22px;
|
|
178
|
+
border-top: 1px solid var(--line);
|
|
179
|
+
display: grid;
|
|
180
|
+
gap: 12px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.connect-section h2 {
|
|
184
|
+
margin: 0;
|
|
185
|
+
font-family: var(--font-heading);
|
|
186
|
+
font-weight: 600;
|
|
187
|
+
font-size: 16px;
|
|
188
|
+
letter-spacing: 0.01em;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.connect-hint {
|
|
192
|
+
margin: 0;
|
|
193
|
+
color: var(--muted);
|
|
194
|
+
font-size: 12px;
|
|
195
|
+
line-height: 1.5;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.connect-hint code {
|
|
199
|
+
font-family: var(--font-mono);
|
|
200
|
+
color: var(--accent);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.connect-snippet {
|
|
204
|
+
position: relative;
|
|
205
|
+
display: grid;
|
|
206
|
+
gap: 8px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.connect-snippet pre {
|
|
210
|
+
margin: 0;
|
|
211
|
+
padding: 14px 16px;
|
|
212
|
+
border: 1px solid var(--line);
|
|
213
|
+
border-radius: 10px;
|
|
214
|
+
background: rgba(10, 14, 26, 0.75);
|
|
215
|
+
overflow-x: auto;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.connect-snippet code {
|
|
219
|
+
font-family: var(--font-mono);
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
line-height: 1.5;
|
|
222
|
+
color: var(--text);
|
|
223
|
+
white-space: pre;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.connect-copy {
|
|
227
|
+
justify-self: start;
|
|
228
|
+
height: 32px;
|
|
229
|
+
padding: 0 14px;
|
|
230
|
+
border: 1px solid var(--line);
|
|
231
|
+
border-radius: 10px;
|
|
232
|
+
background: rgba(19, 26, 42, 0.7);
|
|
233
|
+
color: var(--text);
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.connect-copy:hover,
|
|
240
|
+
.connect-copy:focus {
|
|
241
|
+
border-color: var(--accent);
|
|
242
|
+
background: var(--accent-weak);
|
|
243
|
+
box-shadow: var(--glow-cyan);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.connect-back {
|
|
247
|
+
display: inline-block;
|
|
248
|
+
margin-top: 24px;
|
|
249
|
+
color: var(--muted);
|
|
250
|
+
font-size: 12px;
|
|
251
|
+
text-decoration: none;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.connect-back:hover,
|
|
255
|
+
.connect-back:focus {
|
|
256
|
+
color: var(--accent);
|
|
257
|
+
}`;
|
|
258
|
+
// One inline copy-button behaviour reused for every snippet, mirroring the
|
|
259
|
+
// settings page: prefer the async clipboard API, fall back to a text selection.
|
|
260
|
+
const connectScript = () => `(function () {
|
|
261
|
+
var buttons = document.querySelectorAll('.connect-copy');
|
|
262
|
+
|
|
263
|
+
function flash(button) {
|
|
264
|
+
var original = button.getAttribute('data-label') || 'Copy';
|
|
265
|
+
button.textContent = 'Copied';
|
|
266
|
+
setTimeout(function () {
|
|
267
|
+
button.textContent = original;
|
|
268
|
+
}, 1200);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Array.prototype.forEach.call(buttons, function (button) {
|
|
272
|
+
button.setAttribute('data-label', button.textContent);
|
|
273
|
+
button.addEventListener('click', function () {
|
|
274
|
+
var targetId = button.getAttribute('data-target');
|
|
275
|
+
var target = targetId ? document.getElementById(targetId) : null;
|
|
276
|
+
if (!target) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
var text = target.textContent || '';
|
|
280
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
281
|
+
navigator.clipboard.writeText(text).then(
|
|
282
|
+
function () {
|
|
283
|
+
flash(button);
|
|
284
|
+
},
|
|
285
|
+
function () {
|
|
286
|
+
selectText(target);
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
selectText(target);
|
|
292
|
+
try {
|
|
293
|
+
document.execCommand('copy');
|
|
294
|
+
flash(button);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
// Selection is left in place so the user can copy manually.
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
function selectText(node) {
|
|
302
|
+
if (typeof window.getSelection !== 'function' || typeof document.createRange !== 'function') {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
var selection = window.getSelection();
|
|
306
|
+
var range = document.createRange();
|
|
307
|
+
range.selectNodeContents(node);
|
|
308
|
+
selection.removeAllRanges();
|
|
309
|
+
selection.addRange(range);
|
|
310
|
+
}
|
|
311
|
+
})();`;
|
|
312
|
+
// A snippet block: a titled hint, a <pre><code> with a stable id, and its copy
|
|
313
|
+
// button. The code body is escaped so it survives interpolation verbatim.
|
|
314
|
+
const snippetBlock = (id, title, hint, body) => `
|
|
315
|
+
<div class="connect-snippet">
|
|
316
|
+
<p class="connect-hint"><strong>${escapeHtml(title)}</strong> — ${hint}</p>
|
|
317
|
+
<pre><code id="${escapeHtml(id)}">${escapeHtml(body)}</code></pre>
|
|
318
|
+
<button class="connect-copy" type="button" data-target="${escapeHtml(id)}">Copy</button>
|
|
319
|
+
</div>`;
|
|
320
|
+
const remoteSnippets = (mcpUrl, token) => {
|
|
321
|
+
const codexToml = `[mcp_servers.brainlink]
|
|
322
|
+
command = "npx"
|
|
323
|
+
args = ["-y", "mcp-remote", "${mcpUrl}"]
|
|
324
|
+
|
|
325
|
+
[mcp_servers.brainlink.env]
|
|
326
|
+
MCP_AUTHORIZATION = "Bearer ${token}"`;
|
|
327
|
+
const claudeCmd = `claude mcp add --transport http brainlink ${mcpUrl} --header "Authorization: Bearer ${token}"`;
|
|
328
|
+
const mcpJson = `{
|
|
329
|
+
"mcpServers": {
|
|
330
|
+
"brainlink": {
|
|
331
|
+
"type": "http",
|
|
332
|
+
"url": "${mcpUrl}",
|
|
333
|
+
"headers": {
|
|
334
|
+
"Authorization": "Bearer ${token}"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}`;
|
|
339
|
+
const chatgptCmd = `Connector URL: ${mcpUrl}
|
|
340
|
+
Authorization: Bearer ${token}`;
|
|
341
|
+
return `
|
|
342
|
+
<section class="connect-section" aria-labelledby="codexTitle">
|
|
343
|
+
<h2 id="codexTitle">Codex</h2>
|
|
344
|
+
${snippetBlock('codexSnippet', '~/.codex/config.toml', 'add this MCP server block, then restart Codex.', codexToml)}
|
|
345
|
+
</section>
|
|
346
|
+
<section class="connect-section" aria-labelledby="claudeTitle">
|
|
347
|
+
<h2 id="claudeTitle">Claude Code</h2>
|
|
348
|
+
${snippetBlock('claudeSnippet', 'terminal', 'register the remote server over streamable HTTP.', claudeCmd)}
|
|
349
|
+
</section>
|
|
350
|
+
<section class="connect-section" aria-labelledby="mcpJsonTitle">
|
|
351
|
+
<h2 id="mcpJsonTitle">.mcp.json</h2>
|
|
352
|
+
${snippetBlock('mcpJsonSnippet', 'project .mcp.json', 'drop-in config for any MCP client that reads .mcp.json.', mcpJson)}
|
|
353
|
+
</section>
|
|
354
|
+
<section class="connect-section" aria-labelledby="chatgptTitle">
|
|
355
|
+
<h2 id="chatgptTitle">ChatGPT / generic MCP</h2>
|
|
356
|
+
${snippetBlock('chatgptSnippet', 'connector', 'point a generic MCP connector at the endpoint with a bearer token.', chatgptCmd)}
|
|
357
|
+
</section>`;
|
|
358
|
+
};
|
|
359
|
+
const localSnippets = (vaultPath) => {
|
|
360
|
+
const codexToml = `[mcp_servers.brainlink]
|
|
361
|
+
command = "brainlink"
|
|
362
|
+
args = ["mcp-server", "--vault", "${vaultPath}"]`;
|
|
363
|
+
const claudeCmd = `claude mcp add brainlink -- brainlink mcp-server --vault ${vaultPath}`;
|
|
364
|
+
const mcpJson = `{
|
|
365
|
+
"mcpServers": {
|
|
366
|
+
"brainlink": {
|
|
367
|
+
"command": "brainlink",
|
|
368
|
+
"args": ["mcp-server", "--vault", "${vaultPath}"]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}`;
|
|
372
|
+
return `
|
|
373
|
+
<section class="connect-section" aria-labelledby="codexTitle">
|
|
374
|
+
<h2 id="codexTitle">Codex</h2>
|
|
375
|
+
${snippetBlock('codexSnippet', '~/.codex/config.toml', 'run the local MCP server over stdio, then restart Codex.', codexToml)}
|
|
376
|
+
</section>
|
|
377
|
+
<section class="connect-section" aria-labelledby="claudeTitle">
|
|
378
|
+
<h2 id="claudeTitle">Claude Code</h2>
|
|
379
|
+
${snippetBlock('claudeSnippet', 'terminal', 'register the local stdio server.', claudeCmd)}
|
|
380
|
+
</section>
|
|
381
|
+
<section class="connect-section" aria-labelledby="mcpJsonTitle">
|
|
382
|
+
<h2 id="mcpJsonTitle">.mcp.json</h2>
|
|
383
|
+
${snippetBlock('mcpJsonSnippet', 'project .mcp.json', 'drop-in stdio config for any MCP client that reads .mcp.json.', mcpJson)}
|
|
384
|
+
</section>`;
|
|
385
|
+
};
|
|
386
|
+
const modeBadge = (mode) => mode === 'remote'
|
|
387
|
+
? '<div class="connect-mode">Mode <strong>Remote</strong> · authenticated over HTTPS with your token</div>'
|
|
388
|
+
: '<div class="connect-mode">Mode <strong>Local</strong> · stdio MCP server on this machine</div>';
|
|
389
|
+
const endpointBlock = (data) => data.mode === 'remote'
|
|
390
|
+
? `<div class="connect-endpoint">
|
|
391
|
+
<span>MCP endpoint</span>
|
|
392
|
+
<code>${escapeHtml(data.mcpUrl)}</code>
|
|
393
|
+
<span>Authorization</span>
|
|
394
|
+
<code>Bearer ${escapeHtml(data.token)}</code>
|
|
395
|
+
</div>`
|
|
396
|
+
: `<div class="connect-endpoint">
|
|
397
|
+
<span>Vault path</span>
|
|
398
|
+
<code>${escapeHtml(data.vaultPath)}</code>
|
|
399
|
+
</div>`;
|
|
400
|
+
export const createConnectPageHtml = (data) => {
|
|
401
|
+
const snippets = data.mode === 'remote' ? remoteSnippets(data.mcpUrl, data.token) : localSnippets(data.vaultPath);
|
|
402
|
+
const lede = data.mode === 'remote'
|
|
403
|
+
? 'Wire your agents and chats to this brainlink over the remote MCP endpoint. Paste a snippet, keep your token private.'
|
|
404
|
+
: 'Wire your agents and chats to this brainlink by running the local MCP server against your vault. Paste a snippet to get started.';
|
|
405
|
+
return `<!DOCTYPE html>
|
|
406
|
+
<html lang="en">
|
|
407
|
+
<head>
|
|
408
|
+
<meta charset="utf-8" />
|
|
409
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
410
|
+
<title>Connect your agent · Brainlink</title>
|
|
411
|
+
<style>${connectStyles()}</style>
|
|
412
|
+
</head>
|
|
413
|
+
<body>
|
|
414
|
+
<main class="connect-card">
|
|
415
|
+
<div class="connect-brand">
|
|
416
|
+
<strong>Brainlink</strong>
|
|
417
|
+
<span>Connect your agent</span>
|
|
418
|
+
</div>
|
|
419
|
+
<p class="connect-lede">${escapeHtml(lede)}</p>
|
|
420
|
+
${modeBadge(data.mode)}
|
|
421
|
+
${endpointBlock(data)}
|
|
422
|
+
${snippets}
|
|
423
|
+
<section class="connect-section" aria-labelledby="agentsTitle">
|
|
424
|
+
<h2 id="agentsTitle">Ready AGENTS.md</h2>
|
|
425
|
+
<p class="connect-hint">Drop this operating contract into your agent so it treats this brainlink as canonical memory.</p>
|
|
426
|
+
<div class="connect-snippet">
|
|
427
|
+
<pre><code id="agentsSnippet">${escapeHtml(connectAgentsMarkdown())}</code></pre>
|
|
428
|
+
<button class="connect-copy" type="button" data-target="agentsSnippet">Copy</button>
|
|
429
|
+
</div>
|
|
430
|
+
</section>
|
|
431
|
+
<a class="connect-back" href="/">← Back to graph</a>
|
|
432
|
+
</main>
|
|
433
|
+
<script>${connectScript()}</script>
|
|
434
|
+
</body>
|
|
435
|
+
</html>`;
|
|
436
|
+
};
|
|
@@ -1,167 +1,13 @@
|
|
|
1
1
|
// Self-contained branded auth pages for the graph web server. These are served
|
|
2
|
-
// as plain strings (inline <style> + inline <script>, no external assets
|
|
3
|
-
// framework) so they work identically for a
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--panel-strong: #112130;
|
|
12
|
-
--line: rgba(143, 172, 204, 0.18);
|
|
13
|
-
--text: #edf4ff;
|
|
14
|
-
--muted: #97a9bd;
|
|
15
|
-
--accent: #5aa8ff;
|
|
16
|
-
--accent-weak: rgba(90, 168, 255, 0.18);
|
|
17
|
-
--danger: #ff6b6b;
|
|
18
|
-
--success: #57d9a3;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
* {
|
|
22
|
-
box-sizing: border-box;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
html,
|
|
26
|
-
body {
|
|
27
|
-
width: 100%;
|
|
28
|
-
min-height: 100%;
|
|
29
|
-
margin: 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
body {
|
|
33
|
-
display: flex;
|
|
34
|
-
align-items: center;
|
|
35
|
-
justify-content: center;
|
|
36
|
-
min-height: 100vh;
|
|
37
|
-
min-height: 100dvh;
|
|
38
|
-
padding: 24px;
|
|
39
|
-
color: var(--text);
|
|
40
|
-
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
41
|
-
background:
|
|
42
|
-
linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
43
|
-
linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
44
|
-
radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
|
|
45
|
-
#08131d;
|
|
46
|
-
background-size: 28px 28px, 28px 28px, auto;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
button,
|
|
50
|
-
input {
|
|
51
|
-
font: inherit;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.auth-card {
|
|
55
|
-
width: 100%;
|
|
56
|
-
max-width: 380px;
|
|
57
|
-
padding: 32px 28px;
|
|
58
|
-
border: 1px solid var(--line);
|
|
59
|
-
border-radius: 14px;
|
|
60
|
-
background: rgba(13, 24, 35, 0.92);
|
|
61
|
-
box-shadow: 0 24px 80px rgba(1, 6, 13, 0.48);
|
|
62
|
-
backdrop-filter: blur(8px);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.auth-brand {
|
|
66
|
-
display: grid;
|
|
67
|
-
gap: 4px;
|
|
68
|
-
margin-bottom: 24px;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.auth-brand strong {
|
|
72
|
-
font-size: 26px;
|
|
73
|
-
letter-spacing: 0.01em;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.auth-brand span {
|
|
77
|
-
color: var(--muted);
|
|
78
|
-
font-size: 13px;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.auth-form {
|
|
82
|
-
display: grid;
|
|
83
|
-
gap: 16px;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.auth-field {
|
|
87
|
-
display: grid;
|
|
88
|
-
gap: 7px;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.auth-field label {
|
|
92
|
-
color: var(--muted);
|
|
93
|
-
font-size: 12px;
|
|
94
|
-
letter-spacing: 0.02em;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.auth-field input {
|
|
98
|
-
width: 100%;
|
|
99
|
-
height: 42px;
|
|
100
|
-
padding: 0 14px;
|
|
101
|
-
border: 1px solid var(--line);
|
|
102
|
-
border-radius: 8px;
|
|
103
|
-
outline: none;
|
|
104
|
-
background: rgba(12, 24, 36, 0.94);
|
|
105
|
-
color: var(--text);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.auth-field input:focus {
|
|
109
|
-
border-color: var(--accent);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.auth-submit {
|
|
113
|
-
height: 44px;
|
|
114
|
-
margin-top: 4px;
|
|
115
|
-
border: 1px solid var(--accent);
|
|
116
|
-
border-radius: 8px;
|
|
117
|
-
background: var(--accent-weak);
|
|
118
|
-
color: var(--text);
|
|
119
|
-
cursor: pointer;
|
|
120
|
-
transition: background 120ms ease, opacity 120ms ease;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.auth-submit:hover:not(:disabled),
|
|
124
|
-
.auth-submit:focus:not(:disabled) {
|
|
125
|
-
background: rgba(90, 168, 255, 0.3);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.auth-submit:disabled {
|
|
129
|
-
cursor: progress;
|
|
130
|
-
opacity: 0.6;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.auth-message {
|
|
134
|
-
min-height: 18px;
|
|
135
|
-
margin: 0;
|
|
136
|
-
font-size: 12px;
|
|
137
|
-
line-height: 1.4;
|
|
138
|
-
overflow-wrap: anywhere;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.auth-message[hidden] {
|
|
142
|
-
display: none;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.auth-message.is-error {
|
|
146
|
-
color: var(--danger);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.auth-message.is-success {
|
|
150
|
-
color: var(--success);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.auth-back {
|
|
154
|
-
display: inline-block;
|
|
155
|
-
margin-top: 20px;
|
|
156
|
-
color: var(--muted);
|
|
157
|
-
font-size: 12px;
|
|
158
|
-
text-decoration: none;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.auth-back:hover,
|
|
162
|
-
.auth-back:focus {
|
|
163
|
-
color: var(--accent);
|
|
164
|
-
}`;
|
|
2
|
+
// as plain strings (inline <style> + inline <script>, no external assets beyond
|
|
3
|
+
// the shared Google Fonts @import, no framework) so they work identically for a
|
|
4
|
+
// global npm install. The visual identity is the unified neon system shared
|
|
5
|
+
// with the SaaS app (see neon-theme.ts): #0a0e1a base, cyan #22d3ee + violet
|
|
6
|
+
// #8b5cf6, Space Grotesk headings / DM Sans body, glass card + gradient
|
|
7
|
+
// wordmark + ambient glow, so the app -> SSO -> graph handoff reads as one
|
|
8
|
+
// product.
|
|
9
|
+
import { authSharedStyles } from './auth-styles.js';
|
|
10
|
+
const authStyles = () => authSharedStyles(380);
|
|
165
11
|
const documentShell = (title, body, script) => `<!DOCTYPE html>
|
|
166
12
|
<html lang="en">
|
|
167
13
|
<head>
|