@wipcomputer/wip-ldm-os 0.4.85-alpha.3 → 0.4.85-alpha.32
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/README.md +22 -2
- package/SKILL.md +136 -14
- package/bin/ldm.js +525 -75
- package/docs/universal-installer/SPEC.md +16 -3
- package/docs/universal-installer/TECHNICAL.md +4 -4
- package/lib/deploy.mjs +104 -20
- package/lib/detect.mjs +35 -4
- package/lib/registry-migrations.mjs +296 -0
- package/package.json +24 -3
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-agentid-tenant-boundary.mjs +80 -0
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +129 -0
- package/scripts/test-crc-pair-login-flow.mjs +115 -0
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-pair-status-poll-token.mjs +73 -0
- package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-install-prompt-policy.mjs +84 -0
- package/scripts/test-installer-skill-directory.mjs +55 -0
- package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
- package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
- package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +96 -0
- package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
- package/scripts/test-readme-install-prompt.mjs +66 -0
- package/shared/boot/boot-config.json +18 -8
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
- package/shared/rules/security.md +4 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/boot/README.md +24 -1
- package/src/boot/boot-config.json +18 -8
- package/src/boot/boot-hook.mjs +118 -28
- package/src/boot/installer.mjs +33 -10
- package/src/hosted-mcp/.env.example +4 -0
- package/src/hosted-mcp/README.md +37 -0
- package/src/hosted-mcp/app/footer.js +74 -0
- package/src/hosted-mcp/app/kaleidoscope-login.html +1290 -0
- package/src/hosted-mcp/app/pair.html +165 -57
- package/src/hosted-mcp/app/sprites.png +0 -0
- package/src/hosted-mcp/app/wip-logo.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
- package/src/hosted-mcp/demo/footer.js +2 -2
- package/src/hosted-mcp/demo/index.html +169 -51
- package/src/hosted-mcp/demo/login.html +390 -28
- package/src/hosted-mcp/demo/privacy.html +4 -214
- package/src/hosted-mcp/demo/tos.html +4 -189
- package/src/hosted-mcp/deploy.sh +308 -56
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
- package/src/hosted-mcp/legal/legal-footer.js +75 -0
- package/src/hosted-mcp/legal/legal.css +166 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
- package/src/hosted-mcp/legal/privacy/index.html +253 -0
- package/src/hosted-mcp/nginx/codex-relay.conf +25 -0
- package/src/hosted-mcp/nginx/conf.d/redact-logs.conf +60 -0
- package/src/hosted-mcp/nginx/mcp-oauth.conf +58 -0
- package/src/hosted-mcp/nginx/wip.computer.conf +25 -1
- package/src/hosted-mcp/scripts/audit-logs.sh +205 -0
- package/src/hosted-mcp/scripts/verify-deploy.sh +102 -0
- package/src/hosted-mcp/server.mjs +1681 -166
|
@@ -48,7 +48,7 @@ html, body {
|
|
|
48
48
|
.login-card {
|
|
49
49
|
position: relative;
|
|
50
50
|
max-width: 380px;
|
|
51
|
-
width:
|
|
51
|
+
width: calc(100vw - 48px);
|
|
52
52
|
text-align: center;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -147,14 +147,28 @@ html, body {
|
|
|
147
147
|
</div>
|
|
148
148
|
</div>
|
|
149
149
|
|
|
150
|
-
<!-- Success view -->
|
|
150
|
+
<!-- Success view (legacy login flow) -->
|
|
151
151
|
<div id="success-view" style="display:none">
|
|
152
152
|
<p style="font-size:18px;margin-bottom:12px;color:var(--text);">Welcome, <span id="welcome-name"></span>.</p>
|
|
153
|
-
<p style="color:var(--text-muted);font-size:15px;margin-bottom:8px;">Your passkey has been saved to your phone.</p>
|
|
154
|
-
<p style="color:var(--text-muted);font-size:15px;margin-bottom:24px;">You can use it to sign in to any WIP Computer service.</p>
|
|
155
153
|
<div class="login-buttons">
|
|
156
|
-
<a href="/demo/"
|
|
154
|
+
<a href="/demo/" class="btn btn-primary" style="text-decoration:none;text-align:center;">Try the Demo</a>
|
|
155
|
+
</div>
|
|
156
|
+
<p style="color:var(--text-muted);font-size:15px;margin:18px 0 0;">Your passkey has been saved to your phone. You can use it to sign in to any WIP Computer service.</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Pair-approved view (CRC pair-mode, desktop-side after phone approves) -->
|
|
160
|
+
<div id="pair-approved-view" style="display:none">
|
|
161
|
+
<p style="font-size:18px;margin-bottom:12px;color:var(--text);">Approved on your phone.</p>
|
|
162
|
+
<p style="color:var(--text-muted);font-size:15px;margin-bottom:24px;">Continue pairing on your phone. Your laptop will pick this up after you confirm.</p>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- QR authenticator confirmation view (phone-side after approving another device) -->
|
|
166
|
+
<div id="qr-auth-confirm-view" style="display:none">
|
|
167
|
+
<p style="font-size:18px;margin:0 0 24px;color:var(--text);">Your authenticated Kaleidoscope session is available on your other device.</p>
|
|
168
|
+
<div class="login-buttons">
|
|
169
|
+
<button class="btn btn-primary" id="qrAuthPrimaryBtn" onclick="backToLoginAfterQrScan()">Back to login</button>
|
|
157
170
|
</div>
|
|
171
|
+
<p id="qr-auth-close-message" style="display:none;color:var(--text-muted);font-size:15px;margin:20px 0 0;">You can close this page.</p>
|
|
158
172
|
</div>
|
|
159
173
|
|
|
160
174
|
<div class="login-status" id="loginStatus" style="position:absolute;left:0;right:0;margin-top:16px;text-align:center;"></div>
|
|
@@ -228,12 +242,22 @@ function isSafariDesktop() {
|
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
function needsCustomQR() {
|
|
231
|
-
return !
|
|
245
|
+
return !isLocalPasskeysOn();
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
// ── Local passkeys toggle (persisted in localStorage) ──
|
|
249
|
+
function defaultLocalPasskeysOn() {
|
|
250
|
+
return isMobileDevice();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getLocalPasskeysPreference() {
|
|
254
|
+
var stored = localStorage.getItem('localPasskeys');
|
|
255
|
+
if (stored === 'on' || stored === 'off') return stored;
|
|
256
|
+
return defaultLocalPasskeysOn() ? 'on' : 'off';
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
function isLocalPasskeysOn() {
|
|
236
|
-
return
|
|
260
|
+
return getLocalPasskeysPreference() === 'on';
|
|
237
261
|
}
|
|
238
262
|
|
|
239
263
|
function toggleLocalPasskeys() {
|
|
@@ -257,19 +281,221 @@ function updatePasskeysDot() {
|
|
|
257
281
|
}
|
|
258
282
|
}
|
|
259
283
|
|
|
284
|
+
// ── `next` continuation carrier ──
|
|
285
|
+
//
|
|
286
|
+
// Three whitelisted next shapes:
|
|
287
|
+
//
|
|
288
|
+
// PAIR_NEXT_REGEX /pair/<CODE> using the daemon alphabet
|
|
289
|
+
// (CODEX_PAIR_ALPHABET, length 6, L is
|
|
290
|
+
// included; I/O/0/1 excluded). C8 applies:
|
|
291
|
+
// URL fallback is mobile-only, the desktop
|
|
292
|
+
// must not become the pairing authority.
|
|
293
|
+
//
|
|
294
|
+
// REMOTE_CONTROL_NEXT_REGEX /codex-remote-control/<UUID>. Standard
|
|
295
|
+
// ?next semantics; allowed on both
|
|
296
|
+
// desktop and mobile (this is navigation
|
|
297
|
+
// continuation, not authority transfer).
|
|
298
|
+
//
|
|
299
|
+
// DEMO_NEXT_REGEX /demo. Standard post-login continuation
|
|
300
|
+
// from the homepage CTA.
|
|
301
|
+
//
|
|
302
|
+
// Anything else is silently dropped. `next` is NOT a general redirect
|
|
303
|
+
// primitive. The server validates authoritatively; this client-side
|
|
304
|
+
// check is defense-in-depth.
|
|
305
|
+
var PAIR_NEXT_REGEX = /^\/pair\/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
|
|
306
|
+
var REMOTE_CONTROL_NEXT_REGEX = /^\/codex-remote-control\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
307
|
+
var DEMO_NEXT_REGEX = /^\/demo$/;
|
|
308
|
+
|
|
309
|
+
function isWhitelistedNext(raw) {
|
|
310
|
+
return typeof raw === 'string' && (PAIR_NEXT_REGEX.test(raw) || REMOTE_CONTROL_NEXT_REGEX.test(raw) || DEMO_NEXT_REGEX.test(raw));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readPairNextFromQuery() {
|
|
314
|
+
try {
|
|
315
|
+
var raw = new URLSearchParams(window.location.search).get('next');
|
|
316
|
+
if (!raw) return null;
|
|
317
|
+
return isWhitelistedNext(raw) ? raw : null;
|
|
318
|
+
} catch (e) { return null; }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isPairNextOnDesktop() {
|
|
322
|
+
var next = readPairNextFromQuery();
|
|
323
|
+
return !!(next && PAIR_NEXT_REGEX.test(next) && !isMobileDevice());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Direct Remote Control login continuation. Explicit branch ... NOT
|
|
327
|
+
// routed through pair-mode helpers. Called from doSignIn / doCreateAccount
|
|
328
|
+
// the moment /webauthn/auth-verify or /webauthn/register-verify returns
|
|
329
|
+
// success, BEFORE any qr-login/approve or pair-mode logic runs.
|
|
330
|
+
//
|
|
331
|
+
// If the URL was /login?next=/codex-remote-control/<UUID> AND auth-verify
|
|
332
|
+
// returned a real apiKey, store the credentials in sessionStorage so the
|
|
333
|
+
// destination page's auth gate sees them, then location.replace into the
|
|
334
|
+
// Remote Control surface. Returns true if it redirected; caller must
|
|
335
|
+
// return early.
|
|
336
|
+
//
|
|
337
|
+
// If the URL has no remote-control next, returns false (caller proceeds
|
|
338
|
+
// with the existing QR / pair / welcome logic).
|
|
339
|
+
//
|
|
340
|
+
// If the URL has a remote-control next BUT result.apiKey is missing,
|
|
341
|
+
// console.warn and return false. The caller then falls through to the
|
|
342
|
+
// welcome view (defense in depth from PR #805) so the user is not
|
|
343
|
+
// stranded on a blank /codex-remote-control page that bounces back to
|
|
344
|
+
// sign-in.
|
|
345
|
+
//
|
|
346
|
+
// This is deliberately separate from followPairNextIfPresent because
|
|
347
|
+
// pair-mode behavior (C6 desktop strip, C8 desktop-no-redirect) is
|
|
348
|
+
// orthogonal to standard Remote Control login continuation.
|
|
349
|
+
//
|
|
350
|
+
// Also writes a short-lived same-origin handoff cookie as a Safari-safe
|
|
351
|
+
// fallback: some browsers drop sessionStorage across location.replace
|
|
352
|
+
// under privacy/partition heuristics. Path=/ is required because Safari
|
|
353
|
+
// will silently reject document.cookie writes whose Path attribute is
|
|
354
|
+
// not the current path or an ancestor of it; /login is not an ancestor
|
|
355
|
+
// of /codex-remote-control, so the previous Path=/codex-remote-control
|
|
356
|
+
// scope was dropped on Safari. The Remote Control page reads either
|
|
357
|
+
// source and clears both Path scopes after consumption. See
|
|
358
|
+
// setHandoffCookie below.
|
|
359
|
+
function setHandoffCookie(name, value) {
|
|
360
|
+
document.cookie = name + '=' + encodeURIComponent(value)
|
|
361
|
+
+ '; Path=/'
|
|
362
|
+
+ '; Max-Age=60'
|
|
363
|
+
+ '; Secure'
|
|
364
|
+
+ '; SameSite=Strict';
|
|
365
|
+
}
|
|
366
|
+
function redirectToRemoteControlIfDirectLogin(result) {
|
|
367
|
+
try {
|
|
368
|
+
var raw = new URLSearchParams(window.location.search).get('next');
|
|
369
|
+
if (!raw || !REMOTE_CONTROL_NEXT_REGEX.test(raw)) return false;
|
|
370
|
+
if (!result || !result.apiKey) {
|
|
371
|
+
console.warn('redirectToRemoteControlIfDirectLogin: missing apiKey on result; falling through to welcome view. result=', result);
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
console.log('redirectToRemoteControlIfDirectLogin: storing wip_api_key + redirecting', { agentId: result.agentId, next: raw });
|
|
375
|
+
sessionStorage.setItem('wip_api_key', result.apiKey);
|
|
376
|
+
if (result.agentId) sessionStorage.setItem('wip_handle', result.agentId);
|
|
377
|
+
setHandoffCookie('wip_rc_api_key', result.apiKey);
|
|
378
|
+
if (result.agentId) setHandoffCookie('wip_rc_handle', result.agentId);
|
|
379
|
+
location.replace(raw);
|
|
380
|
+
return true;
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.warn('redirectToRemoteControlIfDirectLogin: unexpected error', e);
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Phone-side post-passkey hook. Called after successful passkey + (optional)
|
|
388
|
+
// qr-login/approve. Decides whether to redirect to the next URL based on
|
|
389
|
+
// authority rules from plan C6 + C8:
|
|
390
|
+
//
|
|
391
|
+
// - QR-scan path (approveResponse.next from the server): follow it for
|
|
392
|
+
// a whitelisted next. The phone scanned the desktop's QR,
|
|
393
|
+
// signed in, and the server returned a sanitized `next`.
|
|
394
|
+
//
|
|
395
|
+
// - URL fallback path (no approveResponse.next):
|
|
396
|
+
// - /codex-remote-control/<UUID>: allowed on both desktop and
|
|
397
|
+
// mobile. Standard navigation continuation.
|
|
398
|
+
// - /pair/<CODE>: mobile-only (C8). Desktop with no
|
|
399
|
+
// approveResponse must not become the pairing authority.
|
|
400
|
+
//
|
|
401
|
+
// REGRESSION COVERED: a desktop browser with "Local passkeys" on,
|
|
402
|
+
// loading /login?next=/pair/<CODE> and completing desktop WebAuthn,
|
|
403
|
+
// MUST NOT redirect to /pair/<CODE> or complete pairing. If a future
|
|
404
|
+
// change opens this gate, it violates plan constraint C8 ("Default
|
|
405
|
+
// authority is phone-held passkey; local desktop passkeys are
|
|
406
|
+
// testing-only"). The codex-remote-control next does NOT trip this
|
|
407
|
+
// rule because it's a navigation continuation, not authority transfer.
|
|
408
|
+
//
|
|
409
|
+
// Returns true if it redirected (caller should not run other view-switching).
|
|
410
|
+
async function followPairNextIfPresent(approveResponse, identity, isNewAccount) {
|
|
411
|
+
var next = null;
|
|
412
|
+
// Path 1: QR phone-side approve. Server-blessed next.
|
|
413
|
+
if (approveResponse && typeof approveResponse.next === 'string' && isWhitelistedNext(approveResponse.next)) {
|
|
414
|
+
if (DEMO_NEXT_REGEX.test(approveResponse.next)) {
|
|
415
|
+
return showQrAuthenticatorConfirmation(identity, isNewAccount === true);
|
|
416
|
+
}
|
|
417
|
+
next = approveResponse.next;
|
|
418
|
+
} else {
|
|
419
|
+
// Path 2: URL ?next= fallback (no approveResponse). Branch by
|
|
420
|
+
// shape: codex-remote-control allowed on any device, pair-mode
|
|
421
|
+
// restricted to mobile per C8.
|
|
422
|
+
var urlNext = readPairNextFromQuery();
|
|
423
|
+
if (urlNext) {
|
|
424
|
+
if (REMOTE_CONTROL_NEXT_REGEX.test(urlNext) || DEMO_NEXT_REGEX.test(urlNext)) {
|
|
425
|
+
next = urlNext;
|
|
426
|
+
} else if (PAIR_NEXT_REGEX.test(urlNext) && isMobileDevice()) {
|
|
427
|
+
next = urlNext;
|
|
428
|
+
}
|
|
429
|
+
// Path 3 (desktop + pair next, no approveResponse): next stays null.
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (!next) return false;
|
|
433
|
+
// Auth-handoff guard: if we don't have an apiKey to store, do NOT
|
|
434
|
+
// redirect to next. Stranding the user on /codex-remote-control/<UUID>
|
|
435
|
+
// without a key in sessionStorage would render a redirect-to-login
|
|
436
|
+
// loop or a blank page (depending on the destination's auth gate).
|
|
437
|
+
// Falling through here lets the caller render the existing
|
|
438
|
+
// welcome-name view, which at least shows the user something they
|
|
439
|
+
// can interact with.
|
|
440
|
+
if (!identity || !identity.apiKey) {
|
|
441
|
+
console.warn('followPairNextIfPresent: missing apiKey on identity; skipping redirect to', next);
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
sessionStorage.setItem('wip_api_key', identity.apiKey);
|
|
445
|
+
if (identity.agentId) sessionStorage.setItem('wip_handle', identity.agentId);
|
|
446
|
+
storeDemoHandoffIfNeeded(next, identity, isNewAccount === true);
|
|
447
|
+
location.replace(next);
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
260
451
|
// ── QR Login (Chrome desktop fallback) ──
|
|
261
452
|
var qrLoginPollTimer = null;
|
|
453
|
+
var qrLoginMode = null;
|
|
454
|
+
|
|
455
|
+
function storeDemoHandoffIfNeeded(next, identity, isNewAccount) {
|
|
456
|
+
if (!DEMO_NEXT_REGEX.test(next) || !identity || !identity.apiKey) return;
|
|
457
|
+
sessionStorage.setItem('lesa-token', identity.apiKey);
|
|
458
|
+
if (identity.agentId) sessionStorage.setItem('lesa-agent', identity.agentId);
|
|
459
|
+
if (identity.tenantId) sessionStorage.setItem('lesa-tenant', identity.tenantId);
|
|
460
|
+
if (isNewAccount) sessionStorage.setItem('lesa-new-account', 'true');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function showQrAuthenticatorConfirmation(identity, isNewAccount) {
|
|
464
|
+
['signup-view', 'qr-view', 'success-view', 'pair-approved-view'].forEach(function(id) {
|
|
465
|
+
var el = document.getElementById(id);
|
|
466
|
+
if (el) el.style.display = 'none';
|
|
467
|
+
});
|
|
468
|
+
var primary = document.getElementById('qrAuthPrimaryBtn');
|
|
469
|
+
if (primary) primary.textContent = 'Back to login';
|
|
470
|
+
var closeMessage = document.getElementById('qr-auth-close-message');
|
|
471
|
+
if (closeMessage) closeMessage.style.display = 'none';
|
|
472
|
+
document.getElementById('qr-auth-confirm-view').style.display = 'block';
|
|
473
|
+
setStatus('', '');
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function backToLoginAfterQrScan() {
|
|
478
|
+
qrSessionId = null;
|
|
479
|
+
qrHandle = null;
|
|
480
|
+
qrMode = 'register';
|
|
481
|
+
qrSessionMode = false;
|
|
482
|
+
location.replace('/login?next=/demo');
|
|
483
|
+
}
|
|
262
484
|
|
|
263
485
|
async function startQrLogin(handle, mode) {
|
|
264
486
|
var btn = document.getElementById('createBtn');
|
|
265
487
|
btn.disabled = true;
|
|
266
488
|
setStatus('', '');
|
|
489
|
+
qrLoginMode = mode || 'register';
|
|
267
490
|
|
|
268
491
|
try {
|
|
492
|
+
var pairNext = readPairNextFromQuery();
|
|
493
|
+
var startBody = { handle: handle || undefined, mode: mode || 'register' };
|
|
494
|
+
if (pairNext) startBody.next = pairNext;
|
|
269
495
|
var res = await fetch('/api/qr-login', {
|
|
270
496
|
method: 'POST',
|
|
271
497
|
headers: { 'Content-Type': 'application/json' },
|
|
272
|
-
body: JSON.stringify(
|
|
498
|
+
body: JSON.stringify(startBody),
|
|
273
499
|
});
|
|
274
500
|
var data = await res.json();
|
|
275
501
|
|
|
@@ -313,8 +539,39 @@ async function pollQrLogin(sessionId) {
|
|
|
313
539
|
clearInterval(qrLoginPollTimer);
|
|
314
540
|
qrLoginPollTimer = null;
|
|
315
541
|
document.getElementById('qr-view').style.display = 'none';
|
|
316
|
-
|
|
317
|
-
|
|
542
|
+
// Branch by next shape. Three cases:
|
|
543
|
+
//
|
|
544
|
+
// 1. Pair-mode (URL ?next=/pair/<CODE>): server stripped apiKey
|
|
545
|
+
// and next from this response (plan C6). Desktop never
|
|
546
|
+
// redirects (C8). Show the dedicated pair-approved view.
|
|
547
|
+
// The phone is the actor that completes pair-complete.
|
|
548
|
+
//
|
|
549
|
+
// 2. Codex remote control continuation (data.next is a
|
|
550
|
+
// /codex-remote-control/<UUID>): standard post-login next.
|
|
551
|
+
// Desktop authenticates with the server-provided apiKey and
|
|
552
|
+
// redirects to next. Server returned the full login response
|
|
553
|
+
// (apiKey, credentialLabel, next) for this case.
|
|
554
|
+
//
|
|
555
|
+
// 3. Legacy login mode (no whitelisted next anywhere): show the
|
|
556
|
+
// existing welcome view.
|
|
557
|
+
var urlNext = readPairNextFromQuery();
|
|
558
|
+
if (urlNext && PAIR_NEXT_REGEX.test(urlNext)) {
|
|
559
|
+
document.getElementById('pair-approved-view').style.display = 'block';
|
|
560
|
+
} else if (data.next && (REMOTE_CONTROL_NEXT_REGEX.test(data.next) || DEMO_NEXT_REGEX.test(data.next)) && data.apiKey) {
|
|
561
|
+
// Standard post-login continuation: pick up the credentials
|
|
562
|
+
// so the destination page is authenticated, then redirect.
|
|
563
|
+
sessionStorage.setItem('wip_api_key', data.apiKey);
|
|
564
|
+
if (data.agentId) sessionStorage.setItem('wip_handle', data.agentId);
|
|
565
|
+
storeDemoHandoffIfNeeded(data.next, data, qrLoginMode === 'register');
|
|
566
|
+
location.replace(data.next);
|
|
567
|
+
} else {
|
|
568
|
+
storeDemoHandoffIfNeeded('/demo', data, qrLoginMode === 'register');
|
|
569
|
+
document.getElementById('success-view').style.display = 'block';
|
|
570
|
+
// Use credentialLabel (matches the saved-passkey label the user
|
|
571
|
+
// sees in iOS Passwords / 1Password). Falls back to agentId for
|
|
572
|
+
// back-compat with older deploys.
|
|
573
|
+
document.getElementById('welcome-name').textContent = data.credentialLabel || data.agentId || 'you';
|
|
574
|
+
}
|
|
318
575
|
}
|
|
319
576
|
} catch (err) {
|
|
320
577
|
// Network error, keep polling
|
|
@@ -326,6 +583,7 @@ function cancelQrLogin() {
|
|
|
326
583
|
clearInterval(qrLoginPollTimer);
|
|
327
584
|
qrLoginPollTimer = null;
|
|
328
585
|
}
|
|
586
|
+
qrLoginMode = null;
|
|
329
587
|
document.getElementById('qr-view').style.display = 'none';
|
|
330
588
|
document.getElementById('signup-view').style.display = 'block';
|
|
331
589
|
document.getElementById('createBtn').disabled = false;
|
|
@@ -339,19 +597,49 @@ var qrHandle = urlParams.get('h');
|
|
|
339
597
|
var qrMode = urlParams.get('m') || 'register';
|
|
340
598
|
var qrSessionMode = !!qrSessionId;
|
|
341
599
|
|
|
600
|
+
// True only while the auto-start setTimeout is invoking
|
|
601
|
+
// doCreateAccount / doSignIn. Read by the NotAllowedError catches so
|
|
602
|
+
// they can suppress the user-facing "Cancelled. Try again when ready."
|
|
603
|
+
// message when the rejection was caused by a strict user-activation
|
|
604
|
+
// policy (Safari Private Browsing) rather than an actual user
|
|
605
|
+
// cancellation. The user's eventual tap on the existing button or
|
|
606
|
+
// link runs the same handler with a real user gesture, and the same
|
|
607
|
+
// qrSessionId completes /api/qr-login/approve.
|
|
608
|
+
var qrAutoStarting = false;
|
|
609
|
+
|
|
342
610
|
if (qrSessionMode) {
|
|
343
611
|
// Clean the URL so session ID doesn't persist
|
|
344
612
|
history.replaceState(null, '', '/login');
|
|
345
613
|
// Pre-fill handle if provided
|
|
346
614
|
if (qrHandle) document.getElementById('handleInput').value = qrHandle;
|
|
347
|
-
// Auto-start the right flow on phone (no extra tap needed)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
615
|
+
// Auto-start the right flow on phone (no extra tap needed).
|
|
616
|
+
// Restored from the pre-#784 known-good QR phone UX. Safari (normal
|
|
617
|
+
// mode) users never see the static login page (Face ID pops within
|
|
618
|
+
// 300ms). Browsers with stricter user-activation policies (Safari
|
|
619
|
+
// Private Browsing, sometimes Chrome on iOS) may reject the
|
|
620
|
+
// auto-start with NotAllowedError. The catch path below replaces
|
|
621
|
+
// the generic "Cancelled" status with a clear, QR-specific
|
|
622
|
+
// instructional message ("This browser blocked automatic Face ID.
|
|
623
|
+
// Tap ... to continue.") and leaves the existing controls enabled
|
|
624
|
+
// so the user's tap completes the flow with a real gesture.
|
|
625
|
+
setTimeout(async function() {
|
|
626
|
+
qrAutoStarting = true;
|
|
627
|
+
try {
|
|
628
|
+
if (qrMode === 'signin') {
|
|
629
|
+
await doSignIn();
|
|
630
|
+
} else {
|
|
631
|
+
await doCreateAccount();
|
|
632
|
+
}
|
|
633
|
+
} finally {
|
|
634
|
+
qrAutoStarting = false;
|
|
353
635
|
}
|
|
354
636
|
}, 300);
|
|
637
|
+
} else if (isPairNextOnDesktop()) {
|
|
638
|
+
// `codex-daemon link` opens /login?next=/pair/<CODE> on the desktop.
|
|
639
|
+
// The desktop's only authority in that flow is to display QR and wait.
|
|
640
|
+
setTimeout(function() {
|
|
641
|
+
startQrLogin('', 'signin');
|
|
642
|
+
}, 0);
|
|
355
643
|
}
|
|
356
644
|
|
|
357
645
|
// ── Create Account ──
|
|
@@ -360,7 +648,14 @@ async function doCreateAccount() {
|
|
|
360
648
|
var btn = document.getElementById('createBtn');
|
|
361
649
|
btn.disabled = true;
|
|
362
650
|
|
|
363
|
-
//
|
|
651
|
+
// Pair-mode desktop URLs must always use the QR path. A local
|
|
652
|
+
// desktop passkey is testing-only and must not become pair authority.
|
|
653
|
+
if (isPairNextOnDesktop() && !qrSessionMode) {
|
|
654
|
+
startQrLogin(username, 'signin');
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Local passkeys off means use WIP's QR cross-device login on any device.
|
|
364
659
|
if (needsCustomQR() && !qrSessionMode) {
|
|
365
660
|
startQrLogin(username);
|
|
366
661
|
return;
|
|
@@ -424,19 +719,38 @@ async function doCreateAccount() {
|
|
|
424
719
|
var result = await verRes.json();
|
|
425
720
|
|
|
426
721
|
if (result.success) {
|
|
722
|
+
// Direct Remote Control login continuation. Runs before any
|
|
723
|
+
// qr-login/approve or pair-mode logic so the apiKey lands in
|
|
724
|
+
// sessionStorage and the destination's auth gate finds it.
|
|
725
|
+
if (redirectToRemoteControlIfDirectLogin(result)) return;
|
|
427
726
|
// If phone completing a desktop QR session, approve it
|
|
727
|
+
var approveResponse = null;
|
|
428
728
|
if (qrSessionMode && qrSessionId) {
|
|
429
|
-
await fetch('/api/qr-login/approve', {
|
|
729
|
+
var approveRes = await fetch('/api/qr-login/approve', {
|
|
430
730
|
method: 'POST',
|
|
431
731
|
headers: { 'Content-Type': 'application/json' },
|
|
432
|
-
body: JSON.stringify({
|
|
732
|
+
body: JSON.stringify({
|
|
733
|
+
sessionId: qrSessionId,
|
|
734
|
+
agentId: result.agentId,
|
|
735
|
+
apiKey: result.apiKey,
|
|
736
|
+
tenantId: result.tenantId,
|
|
737
|
+
credentialLabel: result.credentialLabel,
|
|
738
|
+
}),
|
|
433
739
|
});
|
|
740
|
+
try { approveResponse = await approveRes.json(); } catch (e) { approveResponse = null; }
|
|
434
741
|
}
|
|
435
742
|
// Mark that user has an account (persists across sessions)
|
|
436
743
|
localStorage.setItem('kscope-has-account', 'true');
|
|
744
|
+
// CRC pair-mode: if the server returned a sanitized `next`, store
|
|
745
|
+
// the api_key on the phone and redirect to /pair/<CODE>. Phone is
|
|
746
|
+
// the actor that completes pair-complete (plan C6).
|
|
747
|
+
if (await followPairNextIfPresent(approveResponse, result, true)) return;
|
|
748
|
+
storeDemoHandoffIfNeeded('/demo', result, true);
|
|
437
749
|
document.getElementById('signup-view').style.display = 'none';
|
|
438
750
|
document.getElementById('success-view').style.display = 'block';
|
|
439
|
-
|
|
751
|
+
// Prefer the typed handle for the welcome view. credentialLabel
|
|
752
|
+
// is the saved-passkey label and can be a generated user id.
|
|
753
|
+
document.getElementById('welcome-name').textContent = username || result.credentialLabel || result.agentId || 'you';
|
|
440
754
|
setStatus('', '');
|
|
441
755
|
} else {
|
|
442
756
|
setStatus(result.error || 'Registration failed', 'error');
|
|
@@ -444,8 +758,19 @@ async function doCreateAccount() {
|
|
|
444
758
|
}
|
|
445
759
|
} catch (err) {
|
|
446
760
|
if (err.name === 'NotAllowedError') {
|
|
447
|
-
|
|
448
|
-
|
|
761
|
+
// Distinguish auto-start rejection from a user cancellation.
|
|
762
|
+
// When the QR auto-start hits a strict user-activation policy
|
|
763
|
+
// (Safari Private Browsing, future stricter modes), surface a
|
|
764
|
+
// clear, QR-specific instruction instead of the generic
|
|
765
|
+
// "Cancelled" copy. The existing primary button stays enabled
|
|
766
|
+
// (btn.disabled = false below); the user's tap is a real
|
|
767
|
+
// gesture and reuses the same qrSessionId.
|
|
768
|
+
if (qrSessionMode && qrAutoStarting) {
|
|
769
|
+
setStatus('This browser blocked automatic Face ID. Tap Enter the Kaleidoscope to continue.', 'error');
|
|
770
|
+
} else {
|
|
771
|
+
setStatus('Cancelled. Try again when ready.', 'error');
|
|
772
|
+
setTimeout(function() { clearStatus(); }, 3000);
|
|
773
|
+
}
|
|
449
774
|
} else {
|
|
450
775
|
setStatus('Error: ' + err.message, 'error');
|
|
451
776
|
}
|
|
@@ -457,7 +782,14 @@ async function doSignIn() {
|
|
|
457
782
|
document.getElementById('signInBtn').style.pointerEvents = 'none';
|
|
458
783
|
document.getElementById('createBtn').disabled = true;
|
|
459
784
|
|
|
460
|
-
//
|
|
785
|
+
// Pair-mode desktop URLs must always use the QR path. A local
|
|
786
|
+
// desktop passkey is testing-only and must not become pair authority.
|
|
787
|
+
if (isPairNextOnDesktop() && !qrSessionMode) {
|
|
788
|
+
startQrLogin('', 'signin');
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Local passkeys off means use WIP's QR cross-device login on any device.
|
|
461
793
|
// With local passkeys on, let Chrome show native dialog (supports YubiKey).
|
|
462
794
|
if (needsCustomQR() && !qrSessionMode) {
|
|
463
795
|
startQrLogin('', 'signin');
|
|
@@ -507,17 +839,36 @@ async function doSignIn() {
|
|
|
507
839
|
var result = await verRes.json();
|
|
508
840
|
|
|
509
841
|
if (result.success) {
|
|
842
|
+
// Direct Remote Control login continuation. Runs before any
|
|
843
|
+
// qr-login/approve or pair-mode logic so the apiKey lands in
|
|
844
|
+
// sessionStorage and the destination's auth gate finds it.
|
|
845
|
+
if (redirectToRemoteControlIfDirectLogin(result)) return;
|
|
510
846
|
// If phone completing a desktop QR session, approve it
|
|
847
|
+
var approveResponse = null;
|
|
511
848
|
if (qrSessionMode && qrSessionId) {
|
|
512
|
-
await fetch('/api/qr-login/approve', {
|
|
849
|
+
var approveRes = await fetch('/api/qr-login/approve', {
|
|
513
850
|
method: 'POST',
|
|
514
851
|
headers: { 'Content-Type': 'application/json' },
|
|
515
|
-
body: JSON.stringify({
|
|
852
|
+
body: JSON.stringify({
|
|
853
|
+
sessionId: qrSessionId,
|
|
854
|
+
agentId: result.agentId,
|
|
855
|
+
apiKey: result.apiKey,
|
|
856
|
+
tenantId: result.tenantId,
|
|
857
|
+
credentialLabel: result.credentialLabel,
|
|
858
|
+
}),
|
|
516
859
|
});
|
|
860
|
+
try { approveResponse = await approveRes.json(); } catch (e) { approveResponse = null; }
|
|
517
861
|
}
|
|
862
|
+
// CRC pair-mode: if the server returned a sanitized `next`, store
|
|
863
|
+
// the api_key on the phone and redirect to /pair/<CODE>. Phone is
|
|
864
|
+
// the actor that completes pair-complete (plan C6).
|
|
865
|
+
if (await followPairNextIfPresent(approveResponse, result, false)) return;
|
|
866
|
+
storeDemoHandoffIfNeeded('/demo', result, false);
|
|
518
867
|
document.getElementById('signup-view').style.display = 'none';
|
|
519
868
|
document.getElementById('success-view').style.display = 'block';
|
|
520
|
-
|
|
869
|
+
// Use credentialLabel from the server, which matches the saved-
|
|
870
|
+
// passkey label the user sees in iOS Passwords / 1Password.
|
|
871
|
+
document.getElementById('welcome-name').textContent = result.credentialLabel || result.agentId || 'you';
|
|
521
872
|
setStatus('', '');
|
|
522
873
|
} else {
|
|
523
874
|
setStatus(result.error || 'Authentication failed', 'error');
|
|
@@ -526,8 +877,19 @@ async function doSignIn() {
|
|
|
526
877
|
}
|
|
527
878
|
} catch (err) {
|
|
528
879
|
if (err.name === 'NotAllowedError') {
|
|
529
|
-
|
|
530
|
-
|
|
880
|
+
// Distinguish auto-start rejection from a user cancellation.
|
|
881
|
+
// When the QR auto-start hits a strict user-activation policy
|
|
882
|
+
// (Safari Private Browsing, future stricter modes), surface a
|
|
883
|
+
// clear, QR-specific instruction instead of the generic
|
|
884
|
+
// "Cancelled" copy. The sign-in link and primary button stay
|
|
885
|
+
// enabled below; the user's tap is a real gesture and reuses
|
|
886
|
+
// the same qrSessionId.
|
|
887
|
+
if (qrSessionMode && qrAutoStarting) {
|
|
888
|
+
setStatus('This browser blocked automatic Face ID. Tap Already have an account? Sign in. to continue.', 'error');
|
|
889
|
+
} else {
|
|
890
|
+
setStatus('Cancelled. Try again when ready.', 'error');
|
|
891
|
+
setTimeout(function() { clearStatus(); }, 3000);
|
|
892
|
+
}
|
|
531
893
|
} else {
|
|
532
894
|
setStatus('Error: ' + err.message, 'error');
|
|
533
895
|
}
|