@wipcomputer/wip-ldm-os 0.4.85-alpha.30 → 0.4.85-alpha.33

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.
Files changed (35) hide show
  1. package/bin/ldm.js +187 -1
  2. package/lib/deploy.mjs +36 -1
  3. package/package.json +10 -2
  4. package/scripts/test-boot-dir-truth.mjs +158 -0
  5. package/scripts/test-boot-hook-registration.mjs +136 -0
  6. package/scripts/test-boot-payload-trim.mjs +200 -0
  7. package/scripts/test-crc-pair-login-flow.mjs +76 -1
  8. package/scripts/test-deploy-hook-ownership.mjs +160 -0
  9. package/scripts/test-doctor-hook-dedupe.mjs +188 -0
  10. package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
  11. package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
  12. package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
  13. package/shared/boot/boot-config.json +18 -8
  14. package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
  15. package/shared/rules/security.md +4 -0
  16. package/src/boot/README.md +24 -1
  17. package/src/boot/boot-config.json +18 -8
  18. package/src/boot/boot-hook.mjs +118 -28
  19. package/src/boot/installer.mjs +33 -10
  20. package/src/hosted-mcp/.env.example +4 -0
  21. package/src/hosted-mcp/app/footer.js +2 -2
  22. package/src/hosted-mcp/app/kaleidoscope-login.html +486 -42
  23. package/src/hosted-mcp/app/wip-logo.png +0 -0
  24. package/src/hosted-mcp/demo/footer.js +2 -2
  25. package/src/hosted-mcp/demo/index.html +166 -44
  26. package/src/hosted-mcp/demo/login.html +87 -23
  27. package/src/hosted-mcp/demo/privacy.html +4 -214
  28. package/src/hosted-mcp/demo/tos.html +4 -189
  29. package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
  30. package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
  31. package/src/hosted-mcp/legal/legal-footer.js +75 -0
  32. package/src/hosted-mcp/legal/legal.css +166 -0
  33. package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
  34. package/src/hosted-mcp/legal/privacy/index.html +253 -0
  35. package/src/hosted-mcp/server.mjs +662 -35
@@ -9,9 +9,11 @@
9
9
  *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
10
10
 
11
11
  :root {
12
- --bg: #FFFDF5;
12
+ --bg: #FFFFFF;
13
13
  --text: #1a1a1a;
14
14
  --text-muted: #8a8580;
15
+ --footer-muted: #6b6b6b;
16
+ --footer-border: #ececec;
15
17
  --accent: #0033FF;
16
18
  --accent-hover: #0033FF;
17
19
  --input-bg: #F5F3ED;
@@ -28,10 +30,6 @@ html, body {
28
30
  -webkit-font-smoothing: antialiased;
29
31
  }
30
32
 
31
- @media (min-width: 768px) {
32
- html, body { overflow: hidden; }
33
- }
34
-
35
33
  .login-page {
36
34
  display: flex;
37
35
  flex-direction: column;
@@ -48,7 +46,7 @@ html, body {
48
46
  .login-card {
49
47
  position: relative;
50
48
  max-width: 380px;
51
- width: 100%;
49
+ width: calc(100vw - 48px);
52
50
  text-align: center;
53
51
  }
54
52
 
@@ -112,6 +110,223 @@ html, body {
112
110
  .login-status.loading { background: #E8EEFF; color: var(--accent); }
113
111
  .login-status.error { background: #FFF0F0; color: #D32F2F; }
114
112
  .login-status.success { background: #F0FFF4; color: #2E7D32; }
113
+
114
+ .site-footer {
115
+ padding: 0 0 32px;
116
+ line-height: 1.45;
117
+ }
118
+ .site-footer__inner {
119
+ max-width: 1040px;
120
+ margin: 0 auto;
121
+ padding: 48px 24px 0;
122
+ }
123
+ .site-footer__cols {
124
+ display: grid;
125
+ grid-template-columns: 1fr repeat(3, auto);
126
+ gap: 48px;
127
+ padding-bottom: 40px;
128
+ align-items: start;
129
+ border-top: 1px solid var(--footer-border);
130
+ padding-top: 48px;
131
+ margin-top: -48px;
132
+ }
133
+ .site-footer__col-group {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 28px;
137
+ }
138
+ .site-footer__brand-col {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 4px;
142
+ }
143
+ .site-footer__brand-name {
144
+ font-size: 13px;
145
+ color: var(--text);
146
+ font-weight: 500;
147
+ text-decoration: none;
148
+ transition: opacity 200ms ease;
149
+ }
150
+ .site-footer__brand-name:hover {
151
+ opacity: 0.65;
152
+ }
153
+ .site-footer__brand-tag {
154
+ font-size: 13px;
155
+ }
156
+ .site-footer__brand-tag,
157
+ .site-footer__col a,
158
+ .site-footer__bottom,
159
+ .site-footer__bottom .right a,
160
+ .site-footer__passkeys,
161
+ .site-footer__info,
162
+ .site-footer__link-icon {
163
+ color: var(--footer-muted);
164
+ }
165
+ .site-footer__col-title {
166
+ font-size: 13px;
167
+ font-weight: 600;
168
+ color: var(--text);
169
+ margin-bottom: 14px;
170
+ }
171
+ .site-footer__col ul {
172
+ list-style: none;
173
+ margin: 0;
174
+ padding: 0;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 10px;
178
+ }
179
+ .site-footer__col a {
180
+ font-size: 13px;
181
+ text-decoration: none;
182
+ }
183
+ .site-footer__col a:hover,
184
+ .site-footer__bottom .right a:hover,
185
+ .site-footer__passkeys:hover,
186
+ .site-footer__info:hover,
187
+ .site-footer__link-with-icon:hover .site-footer__link-icon {
188
+ color: var(--text);
189
+ }
190
+ .site-footer__link-with-icon,
191
+ .site-footer__passkeys-row {
192
+ display: inline-flex;
193
+ align-items: center;
194
+ }
195
+ .site-footer__link-with-icon {
196
+ gap: 8px;
197
+ }
198
+ .site-footer__link-icon {
199
+ display: inline-flex;
200
+ width: 14px;
201
+ height: 14px;
202
+ align-items: center;
203
+ justify-content: center;
204
+ }
205
+ .site-footer__passkeys {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ gap: 8px;
209
+ background: transparent;
210
+ border: 0;
211
+ padding: 0;
212
+ font: inherit;
213
+ font-size: 13px;
214
+ cursor: pointer;
215
+ transition: color 180ms ease;
216
+ }
217
+ .site-footer__passkeys-dot {
218
+ width: 7px;
219
+ height: 7px;
220
+ border-radius: 50%;
221
+ background: #c9433a;
222
+ display: inline-block;
223
+ transition: background 200ms ease;
224
+ }
225
+ .site-footer__passkeys.is-on .site-footer__passkeys-dot {
226
+ background: #2f7a5a;
227
+ }
228
+ .site-footer__passkeys-label {
229
+ position: relative;
230
+ display: inline-block;
231
+ }
232
+ .site-footer__passkeys-label > [data-passkeys-label] {
233
+ position: absolute;
234
+ left: 0;
235
+ top: 0;
236
+ white-space: nowrap;
237
+ }
238
+ .site-footer__passkeys-ghost {
239
+ visibility: hidden;
240
+ white-space: nowrap;
241
+ }
242
+ .site-footer__passkeys-row {
243
+ gap: 6px;
244
+ }
245
+ .site-footer__info {
246
+ display: inline-flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ width: 18px;
250
+ height: 18px;
251
+ padding: 0;
252
+ background: transparent;
253
+ border: 0;
254
+ cursor: pointer;
255
+ transition: color 180ms ease;
256
+ }
257
+ .site-footer__info-wrap {
258
+ position: relative;
259
+ display: inline-flex;
260
+ }
261
+ .site-footer__info-popover {
262
+ position: absolute;
263
+ bottom: calc(100% + 8px);
264
+ left: 50%;
265
+ --shift: 0px;
266
+ transform: translateX(calc(-50% + var(--shift))) translateY(4px);
267
+ width: min(240px, calc(100vw - 32px));
268
+ padding: 10px 12px;
269
+ background: var(--text);
270
+ color: var(--bg);
271
+ font-size: 12px;
272
+ line-height: 1.45;
273
+ text-align: left;
274
+ border-radius: 6px;
275
+ opacity: 0;
276
+ pointer-events: none;
277
+ transition: opacity 180ms ease, transform 180ms ease;
278
+ z-index: 20;
279
+ white-space: normal;
280
+ }
281
+ .site-footer__info-popover::after {
282
+ content: "";
283
+ position: absolute;
284
+ top: 100%;
285
+ left: 50%;
286
+ transform: translateX(calc(-50% - var(--shift)));
287
+ border: 5px solid transparent;
288
+ border-top-color: var(--text);
289
+ }
290
+ .site-footer__info[aria-expanded="true"] + .site-footer__info-popover {
291
+ opacity: 1;
292
+ transform: translateX(calc(-50% + var(--shift))) translateY(0);
293
+ }
294
+ .site-footer__bottom {
295
+ border-top: 1px solid var(--footer-border);
296
+ padding-top: 18px;
297
+ display: flex;
298
+ flex-wrap: wrap;
299
+ gap: 16px;
300
+ font-size: 13px;
301
+ }
302
+ .site-footer__bottom .right {
303
+ margin-left: auto;
304
+ display: flex;
305
+ gap: 20px;
306
+ align-items: center;
307
+ }
308
+ .site-footer__bottom .right a {
309
+ text-decoration: none;
310
+ }
311
+ @media (max-width: 960px) {
312
+ .site-footer__cols { gap: 32px; }
313
+ }
314
+ @media (max-width: 720px) {
315
+ .site-footer__cols {
316
+ grid-template-columns: 1fr;
317
+ grid-auto-flow: row;
318
+ gap: 24px;
319
+ }
320
+ .site-footer__col-group { gap: 24px; }
321
+ .site-footer__brand-col,
322
+ .site-footer__col-group:nth-of-type(1),
323
+ .site-footer__col-group:nth-of-type(2),
324
+ .site-footer__col-group:nth-of-type(3) {
325
+ grid-column: 1;
326
+ grid-row: auto;
327
+ justify-self: start;
328
+ }
329
+ }
115
330
  </style>
116
331
  </head>
117
332
  <body>
@@ -147,11 +362,13 @@ html, body {
147
362
  </div>
148
363
  </div>
149
364
 
150
- <!-- Success view (legacy login flow). No action button. -->
365
+ <!-- Success view (legacy login flow) -->
151
366
  <div id="success-view" style="display:none">
152
367
  <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>
368
+ <div class="login-buttons">
369
+ <a href="/demo/" class="btn btn-primary" style="text-decoration:none;text-align:center;">Try the Demo</a>
370
+ </div>
371
+ <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>
155
372
  </div>
156
373
 
157
374
  <!-- Pair-approved view (CRC pair-mode, desktop-side after phone approves) -->
@@ -160,12 +377,127 @@ html, body {
160
377
  <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>
161
378
  </div>
162
379
 
380
+ <!-- QR authenticator confirmation view (phone-side after approving another device) -->
381
+ <div id="qr-auth-confirm-view" style="display:none">
382
+ <p style="font-size:18px;margin:0 0 24px;color:var(--text);">Your authenticated Kaleidoscope session is available on your other device.</p>
383
+ <div class="login-buttons">
384
+ <button class="btn btn-primary" id="qrAuthPrimaryBtn" onclick="backToLoginAfterQrScan()">Back to login</button>
385
+ </div>
386
+ <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>
387
+ </div>
388
+
163
389
  <div class="login-status" id="loginStatus" style="position:absolute;left:0;right:0;margin-top:16px;text-align:center;"></div>
164
390
  </div>
165
391
  </div>
166
- <div id="kscope-footer"></div>
392
+ <footer class="site-footer" data-screen-label="Footer">
393
+ <div class="site-footer__inner">
394
+ <div class="site-footer__cols">
395
+ <div class="site-footer__brand-col">
396
+ <a class="site-footer__brand-name" href="https://wip.computer/">WIP Computer, Inc.</a>
397
+ <div class="site-footer__brand-tag">Learning Dreaming Machines</div>
398
+ <div class="site-footer__brand-tag">Made in California.</div>
399
+ </div>
400
+ <div class="site-footer__col-group">
401
+ <div class="site-footer__col">
402
+ <div class="site-footer__col-title">AI Infrastructure</div>
403
+ <ul>
404
+ <li><a href="https://github.com/wipcomputer/wip-ldm-os" target="_blank" rel="noopener">LDM OS</a></li>
405
+ <li><a href="https://github.com/wipcomputer/memory-crystal" target="_blank" rel="noopener">Memory Crystal</a></li>
406
+ <li><a href="https://github.com/wipcomputer/dream-weaver-protocol" target="_blank" rel="noopener">Dream Weaver</a></li>
407
+ <li><a href="https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/bridge/README.md" target="_blank" rel="noopener">Bridge</a></li>
408
+ </ul>
409
+ </div>
410
+ </div>
411
+ <div class="site-footer__col-group">
412
+ <div class="site-footer__col">
413
+ <div class="site-footer__col-title">AI Skills</div>
414
+ <ul>
415
+ <li><a href="https://github.com/wipcomputer/wip-ldm-os/blob/main/docs/universal-installer/TECHNICAL.md" target="_blank" rel="noopener">Universal Installer</a></li>
416
+ <li><a href="https://github.com/wipcomputer/wip-1password" target="_blank" rel="noopener">1Password</a></li>
417
+ <li><a href="https://github.com/wipcomputer/wip-ai-devops-toolbox" target="_blank" rel="noopener">AI DevOps Toolbox</a></li>
418
+ <li><a href="https://github.com/wipcomputer/wip-x-xai-grok" target="_blank" rel="noopener">X + xAI</a></li>
419
+ </ul>
420
+ </div>
421
+ <div class="site-footer__col">
422
+ <div class="site-footer__col-title">Applications</div>
423
+ <ul>
424
+ <li><a href="https://wip.computer/login?next=/demo" target="_blank" rel="noopener">Kaleidoscope</a></li>
425
+ <li><a href="https://github.com/wipcomputer/wip-codex-remote-control" target="_blank" rel="noopener">Remote Control</a></li>
426
+ <li><a href="https://github.com/wipcomputer/CLVR" target="_blank" rel="noopener">CLVR</a></li>
427
+ <li><a href="https://github.com/wipcomputer/wip-markdown-viewer" target="_blank" rel="noopener">Markdown Viewer</a></li>
428
+ </ul>
429
+ </div>
430
+ </div>
431
+ <div class="site-footer__col-group">
432
+ <div class="site-footer__col">
433
+ <div class="site-footer__col-title">Tools</div>
434
+ <ul>
435
+ <li>
436
+ <a href="https://wip.computer/agent.txt" target="_blank" rel="noopener" data-agent>Are you an AI agent?</a>
437
+ </li>
438
+ <li>
439
+ <a href="https://wip.computer/visualizations/kaleidoscope/onboarding/live/">Visualizations</a>
440
+ </li>
441
+ <li class="site-footer__passkeys-row">
442
+ <button type="button" class="site-footer__passkeys" aria-pressed="false" aria-label="Local passkeys off" data-passkeys>
443
+ <span class="site-footer__passkeys-dot"></span>
444
+ <span class="site-footer__passkeys-label">
445
+ <span data-passkeys-label>Local passkeys off</span>
446
+ <span class="site-footer__passkeys-ghost" aria-hidden="true">Local passkeys off</span>
447
+ </span>
448
+ </button>
449
+ <span class="site-footer__info-wrap">
450
+ <button type="button" class="site-footer__info" aria-label="About local passkeys" data-passkeys-info aria-describedby="passkeys-info-tip">
451
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
452
+ <circle cx="12" cy="12" r="10"></circle>
453
+ <line x1="12" y1="16" x2="12" y2="12"></line>
454
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
455
+ </svg>
456
+ </button>
457
+ <span class="site-footer__info-popover" id="passkeys-info-tip" role="tooltip">
458
+ Local passkeys are off by default on desktop. Your mobile device's passkeys are used for login and device sync. Turn this on to use or save passkeys on this computer.
459
+ </span>
460
+ </span>
461
+ </li>
462
+ </ul>
463
+ </div>
464
+ <div class="site-footer__col">
465
+ <div class="site-footer__col-title">Connect</div>
466
+ <ul>
467
+ <li>
468
+ <a href="https://github.com/wipcomputer" target="_blank" rel="noopener" class="site-footer__link-with-icon">
469
+ <span class="site-footer__link-icon" aria-hidden="true">
470
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
471
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
472
+ </svg>
473
+ </span>
474
+ <span>@wipcomputer</span>
475
+ </a>
476
+ </li>
477
+ <li>
478
+ <a href="https://x.com/wipcomputer" target="_blank" rel="noopener" class="site-footer__link-with-icon">
479
+ <span class="site-footer__link-icon" aria-hidden="true">
480
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
481
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817-5.97 6.817H1.673l7.73-8.835L1.254 2.25h6.83l4.713 6.231 5.447-6.231zM17.083 19.77h1.833L7.084 4.126H5.117l11.966 15.644z"></path>
482
+ </svg>
483
+ </span>
484
+ <span>@wipcomputer</span>
485
+ </a>
486
+ </li>
487
+ </ul>
488
+ </div>
489
+ </div>
490
+ </div>
491
+ <div class="site-footer__bottom">
492
+ <div>Copyright © 2026 WIP Computer, Inc. All rights reserved.</div>
493
+ <div class="right">
494
+ <a href="https://wip.computer/legal/privacy/" target="_blank" rel="noopener">Privacy Policy</a>
495
+ <a href="https://wip.computer/legal/internet-services/terms/site.html" target="_blank" rel="noopener">Terms of Use</a>
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </footer>
167
500
 
168
- <script src="/app/footer.js"></script>
169
501
  <script>
170
502
  // ── Random kaleidoscope icon from sprite sheet ──
171
503
  var SPRITE_COLS = 8;
@@ -231,12 +563,29 @@ function isSafariDesktop() {
231
563
  }
232
564
 
233
565
  function needsCustomQR() {
234
- return !isMobileDevice() && !isSafariDesktop() && !isLocalPasskeysOn();
566
+ return !isLocalPasskeysOn();
235
567
  }
236
568
 
237
569
  // ── Local passkeys toggle (persisted in localStorage) ──
570
+ function defaultLocalPasskeysOn() {
571
+ return isMobileDevice();
572
+ }
573
+
574
+ function getLocalPasskeysPreference() {
575
+ var stored = localStorage.getItem('localPasskeys');
576
+ if (stored === 'on' || stored === 'off') return stored;
577
+ return defaultLocalPasskeysOn() ? 'on' : 'off';
578
+ }
579
+
238
580
  function isLocalPasskeysOn() {
239
- return localStorage.getItem('localPasskeys') === 'on';
581
+ return getLocalPasskeysPreference() === 'on';
582
+ }
583
+
584
+ function localPasskeysInfoCopy() {
585
+ if (defaultLocalPasskeysOn()) {
586
+ return "Local passkeys are on by default on mobile devices. This device's passkeys are used for login and device sync. Turn this off to use or save passkeys on a different device.";
587
+ }
588
+ return "Local passkeys are off by default on desktop. Your mobile device's passkeys are used for login and device sync. Turn this on to use or save passkeys on this computer.";
240
589
  }
241
590
 
242
591
  function toggleLocalPasskeys() {
@@ -246,23 +595,72 @@ function toggleLocalPasskeys() {
246
595
  }
247
596
 
248
597
  function updatePasskeysDot() {
249
- var dot = document.getElementById('passkeys-dot');
250
- var label = document.getElementById('passkeys-label');
251
- if (!dot) return;
252
- if (isLocalPasskeysOn()) {
253
- dot.style.background = '#2E7D32';
254
- dot.style.opacity = '1';
255
- if (label) label.textContent = 'Local passkeys on';
256
- } else {
257
- dot.style.background = '#D32F2F';
258
- dot.style.opacity = '0.4';
259
- if (label) label.textContent = 'Local passkeys off';
598
+ var button = document.querySelector('[data-passkeys]');
599
+ var label = document.querySelector('[data-passkeys-label]');
600
+ if (!button || !label) return;
601
+ var on = isLocalPasskeysOn();
602
+ button.classList.toggle('is-on', on);
603
+ button.setAttribute('aria-pressed', on ? 'true' : 'false');
604
+ button.setAttribute('aria-label', 'Local passkeys ' + (on ? 'on' : 'off'));
605
+ label.textContent = 'Local passkeys ' + (on ? 'on' : 'off');
606
+ }
607
+
608
+ function initFooterPasskeys() {
609
+ var button = document.querySelector('[data-passkeys]');
610
+ if (!button) return;
611
+ button.addEventListener('click', toggleLocalPasskeys);
612
+ updatePasskeysDot();
613
+ }
614
+
615
+ function initFooterPasskeysInfo() {
616
+ var button = document.querySelector('[data-passkeys-info]');
617
+ if (!button) return;
618
+ var popover = document.getElementById(button.getAttribute('aria-describedby'));
619
+ var startScrollY = 0;
620
+ if (popover) popover.textContent = localPasskeysInfoCopy();
621
+
622
+ function setOpen(open) {
623
+ button.setAttribute('aria-expanded', open ? 'true' : 'false');
624
+ if (open) {
625
+ startScrollY = window.scrollY;
626
+ positionPopover();
627
+ }
628
+ }
629
+
630
+ function positionPopover() {
631
+ if (!popover || button.getAttribute('aria-expanded') !== 'true') return;
632
+ popover.style.setProperty('--shift', '0px');
633
+ var rect = popover.getBoundingClientRect();
634
+ var padding = 16;
635
+ var shift = 0;
636
+ if (rect.left < padding) shift = padding - rect.left;
637
+ if (rect.right > window.innerWidth - padding) shift = window.innerWidth - padding - rect.right;
638
+ popover.style.setProperty('--shift', shift + 'px');
260
639
  }
640
+
641
+ button.addEventListener('click', function(event) {
642
+ event.stopPropagation();
643
+ setOpen(button.getAttribute('aria-expanded') !== 'true');
644
+ });
645
+ document.addEventListener('click', function(event) {
646
+ if (!button.contains(event.target) && (!popover || !popover.contains(event.target))) {
647
+ setOpen(false);
648
+ }
649
+ });
650
+ document.addEventListener('keydown', function(event) {
651
+ if (event.key === 'Escape') setOpen(false);
652
+ });
653
+ window.addEventListener('scroll', function() {
654
+ if (button.getAttribute('aria-expanded') === 'true' && Math.abs(window.scrollY - startScrollY) > 24) {
655
+ setOpen(false);
656
+ }
657
+ }, { passive: true });
658
+ window.addEventListener('resize', positionPopover);
261
659
  }
262
660
 
263
661
  // ── `next` continuation carrier ──
264
662
  //
265
- // Two whitelisted next shapes:
663
+ // Three whitelisted next shapes:
266
664
  //
267
665
  // PAIR_NEXT_REGEX /pair/<CODE> using the daemon alphabet
268
666
  // (CODEX_PAIR_ALPHABET, length 6, L is
@@ -275,14 +673,18 @@ function updatePasskeysDot() {
275
673
  // desktop and mobile (this is navigation
276
674
  // continuation, not authority transfer).
277
675
  //
676
+ // DEMO_NEXT_REGEX /demo. Standard post-login continuation
677
+ // from the homepage CTA.
678
+ //
278
679
  // Anything else is silently dropped. `next` is NOT a general redirect
279
680
  // primitive. The server validates authoritatively; this client-side
280
681
  // check is defense-in-depth.
281
682
  var PAIR_NEXT_REGEX = /^\/pair\/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
282
683
  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;
684
+ var DEMO_NEXT_REGEX = /^\/demo$/;
283
685
 
284
686
  function isWhitelistedNext(raw) {
285
- return typeof raw === 'string' && (PAIR_NEXT_REGEX.test(raw) || REMOTE_CONTROL_NEXT_REGEX.test(raw));
687
+ return typeof raw === 'string' && (PAIR_NEXT_REGEX.test(raw) || REMOTE_CONTROL_NEXT_REGEX.test(raw) || DEMO_NEXT_REGEX.test(raw));
286
688
  }
287
689
 
288
690
  function readPairNextFromQuery() {
@@ -364,7 +766,7 @@ function redirectToRemoteControlIfDirectLogin(result) {
364
766
  // authority rules from plan C6 + C8:
365
767
  //
366
768
  // - QR-scan path (approveResponse.next from the server): follow it for
367
- // either whitelisted shape. The phone scanned the desktop's QR,
769
+ // a whitelisted next. The phone scanned the desktop's QR,
368
770
  // signed in, and the server returned a sanitized `next`.
369
771
  //
370
772
  // - URL fallback path (no approveResponse.next):
@@ -382,10 +784,13 @@ function redirectToRemoteControlIfDirectLogin(result) {
382
784
  // rule because it's a navigation continuation, not authority transfer.
383
785
  //
384
786
  // Returns true if it redirected (caller should not run other view-switching).
385
- async function followPairNextIfPresent(approveResponse, identity) {
787
+ async function followPairNextIfPresent(approveResponse, identity, isNewAccount) {
386
788
  var next = null;
387
789
  // Path 1: QR phone-side approve. Server-blessed next, either shape.
388
790
  if (approveResponse && typeof approveResponse.next === 'string' && isWhitelistedNext(approveResponse.next)) {
791
+ if (DEMO_NEXT_REGEX.test(approveResponse.next)) {
792
+ return showQrAuthenticatorConfirmation(identity, isNewAccount === true);
793
+ }
389
794
  next = approveResponse.next;
390
795
  } else {
391
796
  // Path 2: URL ?next= fallback (no approveResponse). Branch by
@@ -393,7 +798,7 @@ async function followPairNextIfPresent(approveResponse, identity) {
393
798
  // restricted to mobile per C8.
394
799
  var urlNext = readPairNextFromQuery();
395
800
  if (urlNext) {
396
- if (REMOTE_CONTROL_NEXT_REGEX.test(urlNext)) {
801
+ if (REMOTE_CONTROL_NEXT_REGEX.test(urlNext) || DEMO_NEXT_REGEX.test(urlNext)) {
397
802
  next = urlNext;
398
803
  } else if (PAIR_NEXT_REGEX.test(urlNext) && isMobileDevice()) {
399
804
  next = urlNext;
@@ -418,17 +823,50 @@ async function followPairNextIfPresent(approveResponse, identity) {
418
823
  if (PAIR_NEXT_REGEX.test(next) && identity.codex_pair_presence_token) {
419
824
  sessionStorage.setItem('wip_codex_pair_presence_token', identity.codex_pair_presence_token);
420
825
  }
826
+ storeDemoHandoffIfNeeded(next, identity, isNewAccount === true);
421
827
  location.replace(next);
422
828
  return true;
423
829
  }
424
830
 
425
831
  // ── QR Login (Chrome desktop fallback) ──
426
832
  var qrLoginPollTimer = null;
833
+ var qrLoginMode = null;
834
+
835
+ function storeDemoHandoffIfNeeded(next, identity, isNewAccount) {
836
+ if (!DEMO_NEXT_REGEX.test(next) || !identity || !identity.apiKey) return;
837
+ sessionStorage.setItem('lesa-token', identity.apiKey);
838
+ if (identity.agentId) sessionStorage.setItem('lesa-agent', identity.agentId);
839
+ if (identity.tenantId) sessionStorage.setItem('lesa-tenant', identity.tenantId);
840
+ if (isNewAccount) sessionStorage.setItem('lesa-new-account', 'true');
841
+ }
842
+
843
+ function showQrAuthenticatorConfirmation(identity, isNewAccount) {
844
+ ['signup-view', 'qr-view', 'success-view', 'pair-approved-view'].forEach(function(id) {
845
+ var el = document.getElementById(id);
846
+ if (el) el.style.display = 'none';
847
+ });
848
+ var primary = document.getElementById('qrAuthPrimaryBtn');
849
+ if (primary) primary.textContent = 'Back to login';
850
+ var closeMessage = document.getElementById('qr-auth-close-message');
851
+ if (closeMessage) closeMessage.style.display = 'none';
852
+ document.getElementById('qr-auth-confirm-view').style.display = 'block';
853
+ setStatus('', '');
854
+ return true;
855
+ }
856
+
857
+ function backToLoginAfterQrScan() {
858
+ qrSessionId = null;
859
+ qrHandle = null;
860
+ qrMode = 'register';
861
+ qrSessionMode = false;
862
+ location.replace('/login?next=/demo');
863
+ }
427
864
 
428
865
  async function startQrLogin(handle, mode) {
429
866
  var btn = document.getElementById('createBtn');
430
867
  btn.disabled = true;
431
868
  setStatus('', '');
869
+ qrLoginMode = mode || 'register';
432
870
 
433
871
  try {
434
872
  var pairNext = readPairNextFromQuery();
@@ -499,13 +937,15 @@ async function pollQrLogin(sessionId) {
499
937
  var urlNext = readPairNextFromQuery();
500
938
  if (urlNext && PAIR_NEXT_REGEX.test(urlNext)) {
501
939
  document.getElementById('pair-approved-view').style.display = 'block';
502
- } else if (data.next && REMOTE_CONTROL_NEXT_REGEX.test(data.next) && data.apiKey) {
503
- // Codex remote control: pick up the credentials so the
504
- // destination page is authenticated, then redirect.
940
+ } else if (data.next && (REMOTE_CONTROL_NEXT_REGEX.test(data.next) || DEMO_NEXT_REGEX.test(data.next)) && data.apiKey) {
941
+ // Standard post-login continuation: pick up the credentials
942
+ // so the destination page is authenticated, then redirect.
505
943
  sessionStorage.setItem('wip_api_key', data.apiKey);
506
944
  if (data.agentId) sessionStorage.setItem('wip_handle', data.agentId);
945
+ storeDemoHandoffIfNeeded(data.next, data, qrLoginMode === 'register');
507
946
  location.replace(data.next);
508
947
  } else {
948
+ storeDemoHandoffIfNeeded('/demo', data, qrLoginMode === 'register');
509
949
  document.getElementById('success-view').style.display = 'block';
510
950
  // Use credentialLabel (matches the saved-passkey label the user
511
951
  // sees in iOS Passwords / 1Password). Falls back to agentId for
@@ -523,6 +963,7 @@ function cancelQrLogin() {
523
963
  clearInterval(qrLoginPollTimer);
524
964
  qrLoginPollTimer = null;
525
965
  }
966
+ qrLoginMode = null;
526
967
  document.getElementById('qr-view').style.display = 'none';
527
968
  document.getElementById('signup-view').style.display = 'block';
528
969
  document.getElementById('createBtn').disabled = false;
@@ -594,7 +1035,7 @@ async function doCreateAccount() {
594
1035
  return;
595
1036
  }
596
1037
 
597
- // Chrome desktop without local passkeys: use custom QR code
1038
+ // Local passkeys off means use WIP's QR cross-device login on any device.
598
1039
  if (needsCustomQR() && !qrSessionMode) {
599
1040
  startQrLogin(username);
600
1041
  return;
@@ -672,6 +1113,7 @@ async function doCreateAccount() {
672
1113
  sessionId: qrSessionId,
673
1114
  agentId: result.agentId,
674
1115
  apiKey: result.apiKey,
1116
+ tenantId: result.tenantId,
675
1117
  credentialLabel: result.credentialLabel,
676
1118
  }),
677
1119
  });
@@ -682,13 +1124,13 @@ async function doCreateAccount() {
682
1124
  // CRC pair-mode: if the server returned a sanitized `next`, store
683
1125
  // the api_key on the phone and redirect to /pair/<CODE>. Phone is
684
1126
  // the actor that completes pair-complete (plan C6).
685
- if (await followPairNextIfPresent(approveResponse, result)) return;
1127
+ if (await followPairNextIfPresent(approveResponse, result, true)) return;
1128
+ storeDemoHandoffIfNeeded('/demo', result, true);
686
1129
  document.getElementById('signup-view').style.display = 'none';
687
1130
  document.getElementById('success-view').style.display = 'block';
688
- // Use credentialLabel from the server, which matches the userName
689
- // saved by iOS Passwords / 1Password. Falls back to typed
690
- // username, then agentId, then "you".
691
- document.getElementById('welcome-name').textContent = result.credentialLabel || username || result.agentId || 'you';
1131
+ // Prefer the typed handle for the welcome view. credentialLabel
1132
+ // is the saved-passkey label and can be a generated user id.
1133
+ document.getElementById('welcome-name').textContent = username || result.credentialLabel || result.agentId || 'you';
692
1134
  setStatus('', '');
693
1135
  } else {
694
1136
  setStatus(result.error || 'Registration failed', 'error');
@@ -727,7 +1169,7 @@ async function doSignIn() {
727
1169
  return;
728
1170
  }
729
1171
 
730
- // Chrome desktop without local passkeys: use QR code for sign-in.
1172
+ // Local passkeys off means use WIP's QR cross-device login on any device.
731
1173
  // With local passkeys on, let Chrome show native dialog (supports YubiKey).
732
1174
  if (needsCustomQR() && !qrSessionMode) {
733
1175
  startQrLogin('', 'signin');
@@ -791,6 +1233,7 @@ async function doSignIn() {
791
1233
  sessionId: qrSessionId,
792
1234
  agentId: result.agentId,
793
1235
  apiKey: result.apiKey,
1236
+ tenantId: result.tenantId,
794
1237
  credentialLabel: result.credentialLabel,
795
1238
  }),
796
1239
  });
@@ -799,7 +1242,8 @@ async function doSignIn() {
799
1242
  // CRC pair-mode: if the server returned a sanitized `next`, store
800
1243
  // the api_key on the phone and redirect to /pair/<CODE>. Phone is
801
1244
  // the actor that completes pair-complete (plan C6).
802
- if (await followPairNextIfPresent(approveResponse, result)) return;
1245
+ if (await followPairNextIfPresent(approveResponse, result, false)) return;
1246
+ storeDemoHandoffIfNeeded('/demo', result, false);
803
1247
  document.getElementById('signup-view').style.display = 'none';
804
1248
  document.getElementById('success-view').style.display = 'block';
805
1249
  // Use credentialLabel from the server, which matches the saved-
@@ -839,8 +1283,8 @@ document.getElementById('handleInput').addEventListener('keydown', function(e) {
839
1283
  if (e.key === 'Enter') doCreateAccount();
840
1284
  });
841
1285
 
842
- // Initialize footer dot
843
- updatePasskeysDot();
1286
+ initFooterPasskeys();
1287
+ initFooterPasskeysInfo();
844
1288
  </script>
845
1289
  </body>
846
1290
  </html>