@vex-chat/spire 4.1.0 → 4.1.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.
|
@@ -23,70 +23,138 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
23
23
|
<style>
|
|
24
24
|
:root {
|
|
25
25
|
color-scheme: dark;
|
|
26
|
-
--bg: #
|
|
27
|
-
--panel: #
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
26
|
+
--bg: #141519;
|
|
27
|
+
--panel: #202126;
|
|
28
|
+
--border: #2b2d33;
|
|
29
|
+
--border-strong: #41434b;
|
|
30
|
+
--text: #f2f3f5;
|
|
31
|
+
--text-secondary: #dbdee1;
|
|
32
|
+
--muted: #b5bac1;
|
|
33
|
+
--faint: #80848e;
|
|
34
|
+
--accent: #e5484d;
|
|
35
|
+
--accent-hover: #f05b60;
|
|
36
|
+
--accent-soft: rgba(229, 72, 77, 0.16);
|
|
37
|
+
--danger: #f23f42;
|
|
38
|
+
--success: #3ba55c;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
* {
|
|
37
42
|
box-sizing: border-box;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
html
|
|
41
|
-
body {
|
|
45
|
+
html {
|
|
42
46
|
min-height: 100%;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
body {
|
|
46
50
|
align-items: center;
|
|
47
|
-
background:
|
|
48
|
-
radial-gradient(circle at 50% 0%, rgba(168, 200, 223, 0.12), transparent 36rem),
|
|
49
|
-
var(--bg);
|
|
51
|
+
background: var(--bg);
|
|
50
52
|
color: var(--text);
|
|
51
53
|
display: flex;
|
|
52
54
|
font-family:
|
|
53
55
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
54
56
|
justify-content: center;
|
|
55
57
|
margin: 0;
|
|
56
|
-
|
|
58
|
+
min-height: 100vh;
|
|
59
|
+
padding: 20px;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
main {
|
|
60
|
-
background:
|
|
61
|
-
border: 1px solid var(--
|
|
63
|
+
background: var(--panel);
|
|
64
|
+
border: 1px solid var(--border);
|
|
62
65
|
border-radius: 8px;
|
|
63
|
-
box-shadow: 0
|
|
64
|
-
max-width:
|
|
65
|
-
|
|
66
|
+
box-shadow: 0 18px 56px rgba(0, 0, 0, 0.34);
|
|
67
|
+
max-width: 420px;
|
|
68
|
+
overflow: hidden;
|
|
66
69
|
width: 100%;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
height:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
.titlebar {
|
|
73
|
+
align-items: center;
|
|
74
|
+
border-bottom: 1px solid var(--border);
|
|
75
|
+
display: flex;
|
|
76
|
+
min-height: 52px;
|
|
77
|
+
padding: 0 20px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.wordmark {
|
|
81
|
+
color: var(--text);
|
|
82
|
+
font-family: "Space Grotesk", Inter, ui-sans-serif, system-ui, sans-serif;
|
|
83
|
+
font-size: 17px;
|
|
84
|
+
font-weight: 700;
|
|
85
|
+
line-height: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.titlebar-divider {
|
|
89
|
+
background: var(--border-strong);
|
|
90
|
+
height: 18px;
|
|
91
|
+
margin: 0 11px;
|
|
92
|
+
width: 1px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.titlebar-context {
|
|
96
|
+
color: var(--muted);
|
|
97
|
+
font-size: 13px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.secure-context {
|
|
102
|
+
align-items: center;
|
|
103
|
+
color: var(--faint);
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
font-size: 12px;
|
|
106
|
+
gap: 5px;
|
|
107
|
+
margin-left: auto;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.secure-context svg {
|
|
111
|
+
height: 14px;
|
|
112
|
+
width: 14px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.content {
|
|
116
|
+
padding: 26px 26px 24px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.passkey-icon {
|
|
120
|
+
align-items: center;
|
|
121
|
+
background: var(--accent-soft);
|
|
122
|
+
border: 1px solid rgba(229, 72, 77, 0.28);
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
color: var(--accent-hover);
|
|
125
|
+
display: flex;
|
|
126
|
+
height: 48px;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
margin-bottom: 20px;
|
|
129
|
+
width: 48px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.passkey-icon svg {
|
|
133
|
+
height: 24px;
|
|
134
|
+
width: 24px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.eyebrow {
|
|
138
|
+
color: var(--faint);
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
font-weight: 700;
|
|
141
|
+
line-height: 1.3;
|
|
142
|
+
margin: 0 0 7px;
|
|
143
|
+
text-transform: uppercase;
|
|
77
144
|
}
|
|
78
145
|
|
|
79
146
|
h1 {
|
|
80
|
-
font-
|
|
81
|
-
font-
|
|
147
|
+
font-family: "Space Grotesk", Inter, ui-sans-serif, system-ui, sans-serif;
|
|
148
|
+
font-size: 23px;
|
|
149
|
+
font-weight: 700;
|
|
82
150
|
letter-spacing: 0;
|
|
83
|
-
line-height: 1.
|
|
84
|
-
margin: 0 0
|
|
151
|
+
line-height: 1.2;
|
|
152
|
+
margin: 0 0 9px;
|
|
85
153
|
}
|
|
86
154
|
|
|
87
|
-
|
|
155
|
+
.description {
|
|
88
156
|
color: var(--muted);
|
|
89
|
-
font-size:
|
|
157
|
+
font-size: 14px;
|
|
90
158
|
line-height: 1.5;
|
|
91
159
|
margin: 0;
|
|
92
160
|
}
|
|
@@ -95,53 +163,229 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
95
163
|
align-items: center;
|
|
96
164
|
appearance: none;
|
|
97
165
|
background: var(--accent);
|
|
98
|
-
border:
|
|
166
|
+
border: 1px solid var(--accent);
|
|
99
167
|
border-radius: 8px;
|
|
100
|
-
color: #
|
|
168
|
+
color: #ffffff;
|
|
101
169
|
cursor: pointer;
|
|
102
170
|
display: inline-flex;
|
|
103
171
|
font: inherit;
|
|
104
|
-
font-
|
|
172
|
+
font-size: 14px;
|
|
173
|
+
font-weight: 650;
|
|
174
|
+
gap: 9px;
|
|
105
175
|
justify-content: center;
|
|
106
|
-
margin-top:
|
|
107
|
-
min-height:
|
|
176
|
+
margin-top: 22px;
|
|
177
|
+
min-height: 44px;
|
|
108
178
|
padding: 0 18px;
|
|
179
|
+
transition:
|
|
180
|
+
background 120ms ease,
|
|
181
|
+
border-color 120ms ease;
|
|
109
182
|
width: 100%;
|
|
110
183
|
}
|
|
111
184
|
|
|
185
|
+
button:hover:not(:disabled) {
|
|
186
|
+
background: var(--accent-hover);
|
|
187
|
+
border-color: var(--accent-hover);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
button:focus-visible {
|
|
191
|
+
outline: 2px solid var(--text);
|
|
192
|
+
outline-offset: 3px;
|
|
193
|
+
}
|
|
194
|
+
|
|
112
195
|
button:disabled {
|
|
113
196
|
cursor: wait;
|
|
114
|
-
opacity: 0.
|
|
197
|
+
opacity: 0.78;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.spinner {
|
|
201
|
+
animation: spin 760ms linear infinite;
|
|
202
|
+
border: 2px solid rgba(255, 255, 255, 0.4);
|
|
203
|
+
border-radius: 50%;
|
|
204
|
+
border-top-color: #ffffff;
|
|
205
|
+
display: none;
|
|
206
|
+
height: 15px;
|
|
207
|
+
width: 15px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
button[data-busy="true"] .spinner {
|
|
211
|
+
display: block;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.helper {
|
|
215
|
+
color: var(--faint);
|
|
216
|
+
font-size: 12px;
|
|
217
|
+
line-height: 1.45;
|
|
218
|
+
margin: 11px 2px 0;
|
|
219
|
+
text-align: center;
|
|
115
220
|
}
|
|
116
221
|
|
|
117
222
|
.status {
|
|
118
|
-
|
|
223
|
+
align-items: flex-start;
|
|
224
|
+
border-top: 1px solid var(--border);
|
|
119
225
|
color: var(--muted);
|
|
120
|
-
|
|
121
|
-
font-size:
|
|
226
|
+
display: flex;
|
|
227
|
+
font-size: 12px;
|
|
228
|
+
gap: 9px;
|
|
122
229
|
line-height: 1.45;
|
|
123
|
-
margin-top:
|
|
230
|
+
margin-top: 20px;
|
|
124
231
|
min-height: 20px;
|
|
125
|
-
padding-top:
|
|
232
|
+
padding-top: 16px;
|
|
126
233
|
word-break: break-word;
|
|
127
234
|
}
|
|
128
235
|
|
|
129
|
-
.
|
|
236
|
+
.status-indicator {
|
|
237
|
+
background: var(--faint);
|
|
238
|
+
border-radius: 2px;
|
|
239
|
+
flex: 0 0 auto;
|
|
240
|
+
height: 7px;
|
|
241
|
+
margin-top: 5px;
|
|
242
|
+
width: 7px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main[data-state="busy"] .status-indicator {
|
|
246
|
+
animation: pulse 1.1s ease-in-out infinite;
|
|
247
|
+
background: var(--accent-hover);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main[data-state="error"] .status {
|
|
130
251
|
color: var(--danger);
|
|
131
252
|
}
|
|
132
253
|
|
|
133
|
-
.
|
|
134
|
-
|
|
254
|
+
main[data-state="error"] .status-indicator {
|
|
255
|
+
background: var(--danger);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main[data-state="success"] .status {
|
|
259
|
+
color: var(--text-secondary);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
main[data-state="success"] .status-indicator {
|
|
263
|
+
background: var(--success);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.completion {
|
|
267
|
+
align-items: center;
|
|
268
|
+
color: var(--text-secondary);
|
|
269
|
+
display: flex;
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
gap: 9px;
|
|
272
|
+
line-height: 1.4;
|
|
273
|
+
margin-top: 22px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.completion svg {
|
|
277
|
+
color: var(--success);
|
|
278
|
+
flex: 0 0 auto;
|
|
279
|
+
height: 20px;
|
|
280
|
+
width: 20px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
[hidden] {
|
|
284
|
+
display: none !important;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@keyframes spin {
|
|
288
|
+
to {
|
|
289
|
+
transform: rotate(360deg);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@keyframes pulse {
|
|
294
|
+
50% {
|
|
295
|
+
opacity: 0.4;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@media (max-width: 460px) {
|
|
300
|
+
body {
|
|
301
|
+
padding: 12px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.content {
|
|
305
|
+
padding: 24px 22px 22px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.titlebar {
|
|
309
|
+
padding: 0 18px;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@media (max-height: 620px) {
|
|
314
|
+
body {
|
|
315
|
+
align-items: flex-start;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.content {
|
|
319
|
+
padding-bottom: 20px;
|
|
320
|
+
padding-top: 22px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.passkey-icon {
|
|
324
|
+
margin-bottom: 16px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
button {
|
|
328
|
+
margin-top: 18px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.status {
|
|
332
|
+
margin-top: 16px;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@media (prefers-reduced-motion: reduce) {
|
|
337
|
+
*,
|
|
338
|
+
*::before,
|
|
339
|
+
*::after {
|
|
340
|
+
animation-duration: 0.01ms !important;
|
|
341
|
+
animation-iteration-count: 1 !important;
|
|
342
|
+
scroll-behavior: auto !important;
|
|
343
|
+
transition-duration: 0.01ms !important;
|
|
344
|
+
}
|
|
135
345
|
}
|
|
136
346
|
</style>
|
|
137
347
|
</head>
|
|
138
348
|
<body>
|
|
139
|
-
<main>
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
349
|
+
<main id="surface" data-state="ready">
|
|
350
|
+
<header class="titlebar">
|
|
351
|
+
<span class="wordmark">vex</span>
|
|
352
|
+
<span class="titlebar-divider" aria-hidden="true"></span>
|
|
353
|
+
<span class="titlebar-context" id="titlebar-context">Passkey</span>
|
|
354
|
+
<span class="secure-context">
|
|
355
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
356
|
+
<rect width="18" height="11" x="3" y="11" rx="2"></rect>
|
|
357
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
358
|
+
</svg>
|
|
359
|
+
Secure
|
|
360
|
+
</span>
|
|
361
|
+
</header>
|
|
362
|
+
<section class="content">
|
|
363
|
+
<div class="passkey-icon">
|
|
364
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
365
|
+
<circle cx="7.5" cy="15.5" r="5.5"></circle>
|
|
366
|
+
<path d="m21 2-9.6 9.6"></path>
|
|
367
|
+
<path d="m15.5 7.5 3 3L22 7l-3-3"></path>
|
|
368
|
+
</svg>
|
|
369
|
+
</div>
|
|
370
|
+
<p class="eyebrow" id="eyebrow">Secure sign in</p>
|
|
371
|
+
<h1 id="title">Continue with your passkey</h1>
|
|
372
|
+
<p class="description" id="copy">Use a passkey saved in this browser or on a nearby device to continue in Vex.</p>
|
|
373
|
+
<button id="action" type="button" data-busy="false">
|
|
374
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
375
|
+
<span id="action-label">Continue with passkey</span>
|
|
376
|
+
</button>
|
|
377
|
+
<p class="helper" id="helper">Your password remains available as a sign-in method.</p>
|
|
378
|
+
<div class="completion" id="completion" hidden>
|
|
379
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round">
|
|
380
|
+
<path d="M20 6 9 17l-5-5"></path>
|
|
381
|
+
</svg>
|
|
382
|
+
<span id="completion-copy">You can close this page and return to Vex.</span>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="status" id="status" role="status" aria-live="polite">
|
|
385
|
+
<span class="status-indicator" aria-hidden="true"></span>
|
|
386
|
+
<span id="status-copy">Ready for your passkey.</span>
|
|
387
|
+
</div>
|
|
388
|
+
</section>
|
|
145
389
|
</main>
|
|
146
390
|
<script>
|
|
147
391
|
"use strict";
|
|
@@ -155,52 +399,137 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
155
399
|
);
|
|
156
400
|
}
|
|
157
401
|
var action = document.getElementById("action");
|
|
158
|
-
var
|
|
402
|
+
var actionLabel = document.getElementById("action-label");
|
|
403
|
+
var completion = document.getElementById("completion");
|
|
404
|
+
var completionCopy = document.getElementById("completion-copy");
|
|
159
405
|
var titleEl = document.getElementById("title");
|
|
160
406
|
var copyEl = document.getElementById("copy");
|
|
407
|
+
var eyebrowEl = document.getElementById("eyebrow");
|
|
408
|
+
var helperEl = document.getElementById("helper");
|
|
409
|
+
var statusCopy = document.getElementById("status-copy");
|
|
410
|
+
var surface = document.getElementById("surface");
|
|
411
|
+
var titlebarContext = document.getElementById("titlebar-context");
|
|
161
412
|
var mode = params.get("mode") || "recover";
|
|
162
413
|
var startupError = null;
|
|
163
414
|
var apiBase = window.location.origin;
|
|
415
|
+
var completionCallback = null;
|
|
416
|
+
var actionDefaultLabel = "Continue with passkey";
|
|
164
417
|
var busy = false;
|
|
165
418
|
var completed = false;
|
|
166
419
|
|
|
420
|
+
configurePage();
|
|
421
|
+
|
|
167
422
|
try {
|
|
168
423
|
apiBase = resolveTrustedApiBase(params.get("api"));
|
|
424
|
+
completionCallback = resolveCompletionCallback(
|
|
425
|
+
params.get("callback"),
|
|
426
|
+
);
|
|
427
|
+
validateLinkParams();
|
|
169
428
|
} catch (err) {
|
|
170
429
|
startupError = err;
|
|
171
430
|
}
|
|
172
431
|
|
|
173
|
-
if (mode === "register" || mode === "register-handoff") {
|
|
174
|
-
titleEl.textContent = "Create a passkey for Vex.";
|
|
175
|
-
copyEl.textContent = "This adds a passkey to your Vex account.";
|
|
176
|
-
action.textContent = "Create passkey";
|
|
177
|
-
setStatus("Ready to create passkey.");
|
|
178
|
-
} else if (mode === "authenticate-handoff") {
|
|
179
|
-
titleEl.textContent = "Continue signing in to Vex.";
|
|
180
|
-
copyEl.textContent =
|
|
181
|
-
"Use your passkey here, then return to the Vex desktop app.";
|
|
182
|
-
setStatus("Ready to verify passkey.");
|
|
183
|
-
} else {
|
|
184
|
-
setStatus("Ready to verify passkey.");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
432
|
if (startupError) {
|
|
188
433
|
setStatus(errorMessage(startupError), "error");
|
|
189
|
-
action.
|
|
434
|
+
action.hidden = true;
|
|
435
|
+
helperEl.textContent =
|
|
436
|
+
"Open a new passkey request from Vex and try again.";
|
|
190
437
|
}
|
|
191
438
|
|
|
192
439
|
action.addEventListener("click", function () {
|
|
193
440
|
void run();
|
|
194
441
|
});
|
|
195
442
|
|
|
443
|
+
function configurePage() {
|
|
444
|
+
document.body.dataset.mode = mode;
|
|
445
|
+
if (mode === "register" || mode === "register-handoff") {
|
|
446
|
+
document.title = "Add a passkey - Vex";
|
|
447
|
+
titlebarContext.textContent = "Add passkey";
|
|
448
|
+
eyebrowEl.textContent = "Account security";
|
|
449
|
+
titleEl.textContent = "Create a passkey";
|
|
450
|
+
copyEl.textContent =
|
|
451
|
+
"Use Face ID, Touch ID, your device PIN, or a security key to add a faster sign-in method.";
|
|
452
|
+
helperEl.textContent =
|
|
453
|
+
"Your password remains available as a sign-in method.";
|
|
454
|
+
actionDefaultLabel = "Create passkey";
|
|
455
|
+
actionLabel.textContent = actionDefaultLabel;
|
|
456
|
+
setStatus("Ready to create your passkey.");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (mode === "authenticate-handoff") {
|
|
460
|
+
document.title = "Sign in with a passkey - Vex";
|
|
461
|
+
titlebarContext.textContent = "Sign in";
|
|
462
|
+
eyebrowEl.textContent = "Desktop sign in";
|
|
463
|
+
titleEl.textContent = "Use your passkey";
|
|
464
|
+
copyEl.textContent =
|
|
465
|
+
"Choose a passkey saved in this browser, on this device, or on a nearby device.";
|
|
466
|
+
helperEl.textContent =
|
|
467
|
+
"Vex will finish signing in automatically after verification.";
|
|
468
|
+
setStatus("Ready to verify your passkey.");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
document.title = "Recover a device - Vex";
|
|
472
|
+
titlebarContext.textContent = "Device recovery";
|
|
473
|
+
eyebrowEl.textContent = "Trusted device";
|
|
474
|
+
titleEl.textContent = "Verify your passkey";
|
|
475
|
+
copyEl.textContent =
|
|
476
|
+
"Use an account passkey to approve this Vex device.";
|
|
477
|
+
helperEl.textContent =
|
|
478
|
+
"Only continue if you started this request from your device.";
|
|
479
|
+
setStatus("Ready to verify your passkey.");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function validateLinkParams() {
|
|
483
|
+
if (mode === "register") {
|
|
484
|
+
requiredParam("token");
|
|
485
|
+
requiredParam("user");
|
|
486
|
+
requiredParam("username");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (mode === "register-handoff" || mode === "authenticate-handoff") {
|
|
490
|
+
requiredParam("token");
|
|
491
|
+
requiredParam("request");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
requiredParam("username");
|
|
495
|
+
requiredParam("request");
|
|
496
|
+
}
|
|
497
|
+
|
|
196
498
|
function setStatus(message, kind) {
|
|
197
|
-
|
|
198
|
-
|
|
499
|
+
statusCopy.textContent = message;
|
|
500
|
+
surface.dataset.state = kind || (busy ? "busy" : "ready");
|
|
199
501
|
}
|
|
200
502
|
|
|
201
503
|
function setBusy(nextBusy) {
|
|
202
504
|
busy = nextBusy;
|
|
203
505
|
action.disabled = nextBusy || completed;
|
|
506
|
+
action.dataset.busy = String(nextBusy);
|
|
507
|
+
surface.setAttribute("aria-busy", String(nextBusy));
|
|
508
|
+
if (nextBusy) {
|
|
509
|
+
surface.dataset.state = "busy";
|
|
510
|
+
actionLabel.textContent = "Waiting for passkey";
|
|
511
|
+
} else {
|
|
512
|
+
if (surface.dataset.state === "busy") {
|
|
513
|
+
surface.dataset.state = "ready";
|
|
514
|
+
}
|
|
515
|
+
actionLabel.textContent = actionDefaultLabel;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function showCompletion(title, copy, status, nextStep) {
|
|
520
|
+
completed = true;
|
|
521
|
+
titleEl.textContent = title;
|
|
522
|
+
copyEl.textContent = copy;
|
|
523
|
+
completionCopy.textContent = nextStep;
|
|
524
|
+
completion.hidden = false;
|
|
525
|
+
action.hidden = true;
|
|
526
|
+
helperEl.hidden = true;
|
|
527
|
+
setStatus(status, "success");
|
|
528
|
+
if (completionCallback) {
|
|
529
|
+
window.setTimeout(function () {
|
|
530
|
+
window.location.assign(completionCallback);
|
|
531
|
+
}, 350);
|
|
532
|
+
}
|
|
204
533
|
}
|
|
205
534
|
|
|
206
535
|
function apiUrl(path) {
|
|
@@ -229,6 +558,26 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
229
558
|
throw new Error("Passkey link API origin is not trusted.");
|
|
230
559
|
}
|
|
231
560
|
|
|
561
|
+
function resolveCompletionCallback(rawCallback) {
|
|
562
|
+
if (!rawCallback) return null;
|
|
563
|
+
var callback;
|
|
564
|
+
try {
|
|
565
|
+
callback = new URL(rawCallback);
|
|
566
|
+
} catch (_err) {
|
|
567
|
+
throw new Error("Passkey completion callback is invalid.");
|
|
568
|
+
}
|
|
569
|
+
if (
|
|
570
|
+
callback.protocol !== "vex:" ||
|
|
571
|
+
callback.hostname !== "passkey" ||
|
|
572
|
+
callback.pathname !== "/complete" ||
|
|
573
|
+
callback.search ||
|
|
574
|
+
callback.hash
|
|
575
|
+
) {
|
|
576
|
+
throw new Error("Passkey completion callback is not trusted.");
|
|
577
|
+
}
|
|
578
|
+
return callback.toString();
|
|
579
|
+
}
|
|
580
|
+
|
|
232
581
|
function isLocalOrigin(url) {
|
|
233
582
|
return (
|
|
234
583
|
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
@@ -255,23 +604,39 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
255
604
|
});
|
|
256
605
|
var text = await response.text();
|
|
257
606
|
var payload = null;
|
|
258
|
-
|
|
607
|
+
var contentType = response.headers.get("content-type") || "";
|
|
608
|
+
if (text.length > 0 && contentType.includes("json")) {
|
|
259
609
|
try {
|
|
260
610
|
payload = JSON.parse(text);
|
|
261
611
|
} catch (_err) {
|
|
262
|
-
payload =
|
|
612
|
+
payload = null;
|
|
263
613
|
}
|
|
264
614
|
}
|
|
265
615
|
if (!response.ok) {
|
|
266
|
-
throw new Error(
|
|
267
|
-
payload && payload.error
|
|
268
|
-
? String(payload.error)
|
|
269
|
-
: "Request failed with status " + response.status,
|
|
270
|
-
);
|
|
616
|
+
throw new Error(apiRequestError(response.status, payload));
|
|
271
617
|
}
|
|
272
618
|
return payload;
|
|
273
619
|
}
|
|
274
620
|
|
|
621
|
+
function apiRequestError(status, payload) {
|
|
622
|
+
if (payload && typeof payload.error === "string") {
|
|
623
|
+
return payload.error;
|
|
624
|
+
}
|
|
625
|
+
if (status === 401 || status === 403) {
|
|
626
|
+
return "This passkey request has expired. Return to Vex and start again.";
|
|
627
|
+
}
|
|
628
|
+
if (status === 404) {
|
|
629
|
+
return "This passkey request is no longer available. Return to Vex and start again.";
|
|
630
|
+
}
|
|
631
|
+
if (status === 429) {
|
|
632
|
+
return "Too many attempts. Wait a moment, then try again.";
|
|
633
|
+
}
|
|
634
|
+
if (status >= 500) {
|
|
635
|
+
return "Vex could not complete the passkey request. Try again.";
|
|
636
|
+
}
|
|
637
|
+
return "The passkey request could not be completed. Try again.";
|
|
638
|
+
}
|
|
639
|
+
|
|
275
640
|
async function run() {
|
|
276
641
|
if (busy) return;
|
|
277
642
|
setBusy(true);
|
|
@@ -289,17 +654,39 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
289
654
|
await recoverWithPasskey();
|
|
290
655
|
}
|
|
291
656
|
} catch (err) {
|
|
292
|
-
|
|
293
|
-
action.textContent =
|
|
657
|
+
actionDefaultLabel =
|
|
294
658
|
mode === "register" || mode === "register-handoff"
|
|
295
659
|
? "Try again"
|
|
296
660
|
: "Retry passkey";
|
|
661
|
+
setStatus(errorMessage(err), "error");
|
|
297
662
|
} finally {
|
|
298
663
|
setBusy(false);
|
|
299
664
|
}
|
|
300
665
|
}
|
|
301
666
|
|
|
302
667
|
function errorMessage(err) {
|
|
668
|
+
if (err && err.name === "NotAllowedError") {
|
|
669
|
+
return mode === "register" || mode === "register-handoff"
|
|
670
|
+
? "Passkey creation was canceled or blocked by this browser. Check the browser prompt, then try again."
|
|
671
|
+
: "Passkey verification was canceled or timed out. Try again when you are ready.";
|
|
672
|
+
}
|
|
673
|
+
if (err && err.name === "InvalidStateError") {
|
|
674
|
+
return "This passkey is already registered with Vex. Try a different passkey.";
|
|
675
|
+
}
|
|
676
|
+
if (err && err.name === "SecurityError") {
|
|
677
|
+
return "This browser cannot use passkeys from the current Vex address.";
|
|
678
|
+
}
|
|
679
|
+
if (err && err.name === "AbortError") {
|
|
680
|
+
return "The passkey request was canceled. Try again when you are ready.";
|
|
681
|
+
}
|
|
682
|
+
if (
|
|
683
|
+
err &&
|
|
684
|
+
(err.name === "NetworkError" ||
|
|
685
|
+
(err.name === "TypeError" &&
|
|
686
|
+
/fetch|network|load failed/i.test(err.message || "")))
|
|
687
|
+
) {
|
|
688
|
+
return "Vex could not reach the server. Check your connection and try again.";
|
|
689
|
+
}
|
|
303
690
|
return err instanceof Error ? err.message : String(err);
|
|
304
691
|
}
|
|
305
692
|
|
|
@@ -326,11 +713,12 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
326
713
|
},
|
|
327
714
|
token: token,
|
|
328
715
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
716
|
+
showCompletion(
|
|
717
|
+
"Passkey saved",
|
|
718
|
+
"Your new passkey is ready to use with Vex.",
|
|
719
|
+
"Passkey added to your account.",
|
|
720
|
+
"Return to the Vex CLI. It will finish connecting automatically.",
|
|
721
|
+
);
|
|
334
722
|
}
|
|
335
723
|
|
|
336
724
|
async function registerPasskeyWithHandoff() {
|
|
@@ -360,11 +748,12 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
360
748
|
},
|
|
361
749
|
},
|
|
362
750
|
);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
751
|
+
showCompletion(
|
|
752
|
+
"Passkey saved",
|
|
753
|
+
"Your new passkey is ready to use with Vex.",
|
|
754
|
+
"Passkey added to your account.",
|
|
755
|
+
"You can close this page and return to Vex.",
|
|
756
|
+
);
|
|
368
757
|
}
|
|
369
758
|
|
|
370
759
|
async function createPasskeyCredential(options) {
|
|
@@ -409,11 +798,12 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
409
798
|
},
|
|
410
799
|
},
|
|
411
800
|
);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
801
|
+
showCompletion(
|
|
802
|
+
"You are verified",
|
|
803
|
+
"Vex can finish signing you in on the desktop.",
|
|
804
|
+
"Passkey verified successfully.",
|
|
805
|
+
"You can close this page and return to Vex.",
|
|
806
|
+
);
|
|
417
807
|
}
|
|
418
808
|
|
|
419
809
|
async function recoverWithPasskey() {
|
|
@@ -462,11 +852,12 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
|
|
|
462
852
|
token: token,
|
|
463
853
|
},
|
|
464
854
|
);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
855
|
+
showCompletion(
|
|
856
|
+
"Device approved",
|
|
857
|
+
"Your Vex device can finish connecting now.",
|
|
858
|
+
"Device approved with your passkey.",
|
|
859
|
+
"Return to the Vex CLI. It will finish connecting automatically.",
|
|
860
|
+
);
|
|
470
861
|
}
|
|
471
862
|
|
|
472
863
|
function requiredParam(name) {
|