@wipcomputer/wip-ldm-os 0.4.85-alpha.8 → 0.4.86

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 (57) hide show
  1. package/README.md +22 -2
  2. package/SKILL.md +137 -15
  3. package/bin/ldm.js +608 -75
  4. package/lib/deploy.mjs +90 -12
  5. package/lib/registry-migrations.mjs +296 -0
  6. package/package.json +21 -3
  7. package/scripts/test-boot-dir-truth.mjs +158 -0
  8. package/scripts/test-boot-hook-registration.mjs +136 -0
  9. package/scripts/test-boot-payload-trim.mjs +200 -0
  10. package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
  11. package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
  12. package/scripts/test-crc-e2ee-session-route.mjs +9 -2
  13. package/scripts/test-crc-pair-login-flow.mjs +76 -1
  14. package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
  15. package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
  16. package/scripts/test-deploy-hook-ownership.mjs +160 -0
  17. package/scripts/test-doctor-hook-dedupe.mjs +188 -0
  18. package/scripts/test-install-prompt-policy.mjs +84 -0
  19. package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
  20. package/scripts/test-installer-target-self-update.mjs +131 -0
  21. package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
  22. package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
  23. package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
  24. package/scripts/test-ldm-status-concurrency.mjs +118 -0
  25. package/scripts/test-ldm-status-timeout.mjs +96 -0
  26. package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
  27. package/scripts/test-readme-install-prompt.mjs +66 -0
  28. package/shared/boot/boot-config.json +18 -8
  29. package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
  30. package/shared/rules/security.md +4 -0
  31. package/shared/templates/install-prompt.md +20 -2
  32. package/src/boot/README.md +24 -1
  33. package/src/boot/boot-config.json +18 -8
  34. package/src/boot/boot-hook.mjs +118 -28
  35. package/src/boot/installer.mjs +33 -10
  36. package/src/hosted-mcp/.env.example +4 -0
  37. package/src/hosted-mcp/README.md +37 -0
  38. package/src/hosted-mcp/app/footer.js +2 -2
  39. package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
  40. package/src/hosted-mcp/app/pair.html +21 -3
  41. package/src/hosted-mcp/app/wip-logo.png +0 -0
  42. package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
  43. package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
  44. package/src/hosted-mcp/demo/footer.js +2 -2
  45. package/src/hosted-mcp/demo/index.html +166 -44
  46. package/src/hosted-mcp/demo/login.html +87 -23
  47. package/src/hosted-mcp/demo/privacy.html +4 -214
  48. package/src/hosted-mcp/demo/tos.html +4 -189
  49. package/src/hosted-mcp/deploy.sh +2 -0
  50. package/src/hosted-mcp/docs/self-host.md +268 -0
  51. package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
  52. package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
  53. package/src/hosted-mcp/legal/legal-footer.js +75 -0
  54. package/src/hosted-mcp/legal/legal.css +166 -0
  55. package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
  56. package/src/hosted-mcp/legal/privacy/index.html +253 -0
  57. package/src/hosted-mcp/server.mjs +920 -74
@@ -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;
@@ -415,17 +820,53 @@ async function followPairNextIfPresent(approveResponse, identity) {
415
820
  }
416
821
  sessionStorage.setItem('wip_api_key', identity.apiKey);
417
822
  if (identity.agentId) sessionStorage.setItem('wip_handle', identity.agentId);
823
+ if (PAIR_NEXT_REGEX.test(next) && identity.codex_pair_presence_token) {
824
+ sessionStorage.setItem('wip_codex_pair_presence_token', identity.codex_pair_presence_token);
825
+ }
826
+ storeDemoHandoffIfNeeded(next, identity, isNewAccount === true);
418
827
  location.replace(next);
419
828
  return true;
420
829
  }
421
830
 
422
831
  // ── QR Login (Chrome desktop fallback) ──
423
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
+ }
424
864
 
425
865
  async function startQrLogin(handle, mode) {
426
866
  var btn = document.getElementById('createBtn');
427
867
  btn.disabled = true;
428
868
  setStatus('', '');
869
+ qrLoginMode = mode || 'register';
429
870
 
430
871
  try {
431
872
  var pairNext = readPairNextFromQuery();
@@ -496,13 +937,15 @@ async function pollQrLogin(sessionId) {
496
937
  var urlNext = readPairNextFromQuery();
497
938
  if (urlNext && PAIR_NEXT_REGEX.test(urlNext)) {
498
939
  document.getElementById('pair-approved-view').style.display = 'block';
499
- } else if (data.next && REMOTE_CONTROL_NEXT_REGEX.test(data.next) && data.apiKey) {
500
- // Codex remote control: pick up the credentials so the
501
- // 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.
502
943
  sessionStorage.setItem('wip_api_key', data.apiKey);
503
944
  if (data.agentId) sessionStorage.setItem('wip_handle', data.agentId);
945
+ storeDemoHandoffIfNeeded(data.next, data, qrLoginMode === 'register');
504
946
  location.replace(data.next);
505
947
  } else {
948
+ storeDemoHandoffIfNeeded('/demo', data, qrLoginMode === 'register');
506
949
  document.getElementById('success-view').style.display = 'block';
507
950
  // Use credentialLabel (matches the saved-passkey label the user
508
951
  // sees in iOS Passwords / 1Password). Falls back to agentId for
@@ -520,6 +963,7 @@ function cancelQrLogin() {
520
963
  clearInterval(qrLoginPollTimer);
521
964
  qrLoginPollTimer = null;
522
965
  }
966
+ qrLoginMode = null;
523
967
  document.getElementById('qr-view').style.display = 'none';
524
968
  document.getElementById('signup-view').style.display = 'block';
525
969
  document.getElementById('createBtn').disabled = false;
@@ -591,7 +1035,7 @@ async function doCreateAccount() {
591
1035
  return;
592
1036
  }
593
1037
 
594
- // Chrome desktop without local passkeys: use custom QR code
1038
+ // Local passkeys off means use WIP's QR cross-device login on any device.
595
1039
  if (needsCustomQR() && !qrSessionMode) {
596
1040
  startQrLogin(username);
597
1041
  return;
@@ -669,6 +1113,7 @@ async function doCreateAccount() {
669
1113
  sessionId: qrSessionId,
670
1114
  agentId: result.agentId,
671
1115
  apiKey: result.apiKey,
1116
+ tenantId: result.tenantId,
672
1117
  credentialLabel: result.credentialLabel,
673
1118
  }),
674
1119
  });
@@ -679,13 +1124,13 @@ async function doCreateAccount() {
679
1124
  // CRC pair-mode: if the server returned a sanitized `next`, store
680
1125
  // the api_key on the phone and redirect to /pair/<CODE>. Phone is
681
1126
  // the actor that completes pair-complete (plan C6).
682
- if (await followPairNextIfPresent(approveResponse, result)) return;
1127
+ if (await followPairNextIfPresent(approveResponse, result, true)) return;
1128
+ storeDemoHandoffIfNeeded('/demo', result, true);
683
1129
  document.getElementById('signup-view').style.display = 'none';
684
1130
  document.getElementById('success-view').style.display = 'block';
685
- // Use credentialLabel from the server, which matches the userName
686
- // saved by iOS Passwords / 1Password. Falls back to typed
687
- // username, then agentId, then "you".
688
- 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';
689
1134
  setStatus('', '');
690
1135
  } else {
691
1136
  setStatus(result.error || 'Registration failed', 'error');
@@ -724,7 +1169,7 @@ async function doSignIn() {
724
1169
  return;
725
1170
  }
726
1171
 
727
- // 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.
728
1173
  // With local passkeys on, let Chrome show native dialog (supports YubiKey).
729
1174
  if (needsCustomQR() && !qrSessionMode) {
730
1175
  startQrLogin('', 'signin');
@@ -788,6 +1233,7 @@ async function doSignIn() {
788
1233
  sessionId: qrSessionId,
789
1234
  agentId: result.agentId,
790
1235
  apiKey: result.apiKey,
1236
+ tenantId: result.tenantId,
791
1237
  credentialLabel: result.credentialLabel,
792
1238
  }),
793
1239
  });
@@ -796,7 +1242,8 @@ async function doSignIn() {
796
1242
  // CRC pair-mode: if the server returned a sanitized `next`, store
797
1243
  // the api_key on the phone and redirect to /pair/<CODE>. Phone is
798
1244
  // the actor that completes pair-complete (plan C6).
799
- if (await followPairNextIfPresent(approveResponse, result)) return;
1245
+ if (await followPairNextIfPresent(approveResponse, result, false)) return;
1246
+ storeDemoHandoffIfNeeded('/demo', result, false);
800
1247
  document.getElementById('signup-view').style.display = 'none';
801
1248
  document.getElementById('success-view').style.display = 'block';
802
1249
  // Use credentialLabel from the server, which matches the saved-
@@ -836,8 +1283,8 @@ document.getElementById('handleInput').addEventListener('keydown', function(e) {
836
1283
  if (e.key === 'Enter') doCreateAccount();
837
1284
  });
838
1285
 
839
- // Initialize footer dot
840
- updatePasskeysDot();
1286
+ initFooterPasskeys();
1287
+ initFooterPasskeysInfo();
841
1288
  </script>
842
1289
  </body>
843
1290
  </html>