@vellumai/cli 0.8.8 → 0.8.9-staging.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/package.json +1 -1
- package/src/commands/login.ts +128 -9
package/package.json
CHANGED
package/src/commands/login.ts
CHANGED
|
@@ -32,6 +32,131 @@ import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
|
32
32
|
|
|
33
33
|
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
34
34
|
|
|
35
|
+
function escapeHtml(s: string): string {
|
|
36
|
+
return s
|
|
37
|
+
.replace(/&/g, "&")
|
|
38
|
+
.replace(/</g, "<")
|
|
39
|
+
.replace(/>/g, ">")
|
|
40
|
+
.replace(/"/g, """)
|
|
41
|
+
.replace(/'/g, "'");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderLoginPage(title: string, subtitle: string, success: boolean): string {
|
|
45
|
+
const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
46
|
+
<circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
|
|
47
|
+
<path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
48
|
+
</svg>`;
|
|
49
|
+
|
|
50
|
+
const errorSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
51
|
+
<circle cx="28" cy="28" r="28" fill="var(--negative-bg)"/>
|
|
52
|
+
<path class="cross cross-1" d="M20 20L36 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
53
|
+
<path class="cross cross-2" d="M36 20L20 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
54
|
+
</svg>`;
|
|
55
|
+
|
|
56
|
+
return `<!DOCTYPE html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="utf-8">
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
61
|
+
<title>${escapeHtml(title)}</title>
|
|
62
|
+
<style>
|
|
63
|
+
:root {
|
|
64
|
+
--surface: #F5F3EB;
|
|
65
|
+
--surface-card: #FFFFFF;
|
|
66
|
+
--card-border: #E8E6DA;
|
|
67
|
+
--text-primary: #2A2A28;
|
|
68
|
+
--text-secondary: #4A4A46;
|
|
69
|
+
--positive-bg: #D4DFD0;
|
|
70
|
+
--positive-fg: #516748;
|
|
71
|
+
--negative-bg: #F7DAC9;
|
|
72
|
+
--negative-fg: #DA491A;
|
|
73
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
|
|
74
|
+
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
75
|
+
}
|
|
76
|
+
@media (prefers-color-scheme: dark) {
|
|
77
|
+
:root {
|
|
78
|
+
--surface: #1A1A18;
|
|
79
|
+
--surface-card: #2A2A28;
|
|
80
|
+
--card-border: #3A3A37;
|
|
81
|
+
--text-primary: #F5F3EB;
|
|
82
|
+
--text-secondary: #BDB9A9;
|
|
83
|
+
--positive-bg: #1A2316;
|
|
84
|
+
--positive-fg: #7A8B6F;
|
|
85
|
+
--negative-bg: #4E281D;
|
|
86
|
+
--negative-fg: #E86B40;
|
|
87
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
91
|
+
body {
|
|
92
|
+
font-family: var(--font);
|
|
93
|
+
background: var(--surface);
|
|
94
|
+
color: var(--text-primary);
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
min-height: 100vh;
|
|
99
|
+
-webkit-font-smoothing: antialiased;
|
|
100
|
+
}
|
|
101
|
+
.card {
|
|
102
|
+
text-align: center;
|
|
103
|
+
padding: 48px 40px 40px;
|
|
104
|
+
background: var(--surface-card);
|
|
105
|
+
border: 1px solid var(--card-border);
|
|
106
|
+
border-radius: 16px;
|
|
107
|
+
box-shadow: var(--shadow);
|
|
108
|
+
max-width: 380px;
|
|
109
|
+
width: 100%;
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transform: translateY(8px) scale(0.98);
|
|
112
|
+
animation: cardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
|
|
113
|
+
}
|
|
114
|
+
@keyframes cardIn {
|
|
115
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
116
|
+
}
|
|
117
|
+
.icon {
|
|
118
|
+
width: 56px;
|
|
119
|
+
height: 56px;
|
|
120
|
+
margin-bottom: 20px;
|
|
121
|
+
}
|
|
122
|
+
.check {
|
|
123
|
+
stroke-dasharray: 32;
|
|
124
|
+
stroke-dashoffset: 32;
|
|
125
|
+
animation: draw 0.4s ease-out 0.45s forwards;
|
|
126
|
+
}
|
|
127
|
+
.cross {
|
|
128
|
+
stroke-dasharray: 22;
|
|
129
|
+
stroke-dashoffset: 22;
|
|
130
|
+
}
|
|
131
|
+
.cross-1 { animation: draw 0.3s ease-out 0.45s forwards; }
|
|
132
|
+
.cross-2 { animation: draw 0.3s ease-out 0.55s forwards; }
|
|
133
|
+
@keyframes draw {
|
|
134
|
+
to { stroke-dashoffset: 0; }
|
|
135
|
+
}
|
|
136
|
+
h1 {
|
|
137
|
+
font-size: 18px;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
letter-spacing: -0.2px;
|
|
140
|
+
color: var(--text-primary);
|
|
141
|
+
margin-bottom: 6px;
|
|
142
|
+
}
|
|
143
|
+
p {
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
line-height: 1.5;
|
|
146
|
+
color: var(--text-secondary);
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
<div class="card">
|
|
152
|
+
${success ? checkmarkSvg : errorSvg}
|
|
153
|
+
<h1>${escapeHtml(title)}</h1>
|
|
154
|
+
<p>${escapeHtml(subtitle)}</p>
|
|
155
|
+
</div>
|
|
156
|
+
</body>
|
|
157
|
+
</html>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
35
160
|
/**
|
|
36
161
|
* Open a URL in the user's default browser.
|
|
37
162
|
*/
|
|
@@ -72,26 +197,20 @@ function browserLogin(webUrl: string): Promise<string> {
|
|
|
72
197
|
|
|
73
198
|
if (receivedState !== state) {
|
|
74
199
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
75
|
-
res.end(
|
|
76
|
-
"<html><body><h2>Login failed</h2><p>State mismatch. Please try again.</p></body></html>",
|
|
77
|
-
);
|
|
200
|
+
res.end(renderLoginPage("Login Failed", "State mismatch. Please try again.", false));
|
|
78
201
|
cleanup("State mismatch — possible CSRF attack.");
|
|
79
202
|
return;
|
|
80
203
|
}
|
|
81
204
|
|
|
82
205
|
if (!sessionToken) {
|
|
83
206
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
84
|
-
res.end(
|
|
85
|
-
"<html><body><h2>Login failed</h2><p>No session token received. Please try again.</p></body></html>",
|
|
86
|
-
);
|
|
207
|
+
res.end(renderLoginPage("Login Failed", "No session token received. Please try again.", false));
|
|
87
208
|
cleanup("No session token received from platform.");
|
|
88
209
|
return;
|
|
89
210
|
}
|
|
90
211
|
|
|
91
212
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
92
|
-
res.end(
|
|
93
|
-
"<html><body><h2>Login successful!</h2><p>You can close this window and return to your terminal.</p></body></html>",
|
|
94
|
-
);
|
|
213
|
+
res.end(renderLoginPage("Login Successful", "You can close this window and return to your terminal.", true));
|
|
95
214
|
cleanup(null, sessionToken);
|
|
96
215
|
});
|
|
97
216
|
|