@wipcomputer/wip-ldm-os 0.4.85-alpha.30 → 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.
Files changed (32) hide show
  1. package/bin/ldm.js +103 -0
  2. package/package.json +8 -2
  3. package/scripts/test-boot-hook-registration.mjs +136 -0
  4. package/scripts/test-boot-payload-trim.mjs +200 -0
  5. package/scripts/test-crc-pair-login-flow.mjs +76 -1
  6. package/scripts/test-doctor-hook-dedupe.mjs +188 -0
  7. package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
  8. package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
  9. package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
  10. package/shared/boot/boot-config.json +18 -8
  11. package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
  12. package/shared/rules/security.md +4 -0
  13. package/src/boot/README.md +24 -1
  14. package/src/boot/boot-config.json +18 -8
  15. package/src/boot/boot-hook.mjs +118 -28
  16. package/src/boot/installer.mjs +33 -10
  17. package/src/hosted-mcp/.env.example +4 -0
  18. package/src/hosted-mcp/app/footer.js +2 -2
  19. package/src/hosted-mcp/app/kaleidoscope-login.html +486 -42
  20. package/src/hosted-mcp/app/wip-logo.png +0 -0
  21. package/src/hosted-mcp/demo/footer.js +2 -2
  22. package/src/hosted-mcp/demo/index.html +166 -44
  23. package/src/hosted-mcp/demo/login.html +87 -23
  24. package/src/hosted-mcp/demo/privacy.html +4 -214
  25. package/src/hosted-mcp/demo/tos.html +4 -189
  26. package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
  27. package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
  28. package/src/hosted-mcp/legal/legal-footer.js +75 -0
  29. package/src/hosted-mcp/legal/legal.css +166 -0
  30. package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
  31. package/src/hosted-mcp/legal/privacy/index.html +253 -0
  32. package/src/hosted-mcp/server.mjs +662 -35
@@ -48,7 +48,7 @@ html, body {
48
48
  .login-card {
49
49
  position: relative;
50
50
  max-width: 380px;
51
- width: 100%;
51
+ width: calc(100vw - 48px);
52
52
  text-align: center;
53
53
  }
54
54
 
@@ -150,11 +150,10 @@ html, body {
150
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/" onclick="sessionStorage.clear();" class="btn btn-primary" style="text-decoration:none;text-align:center;">Try the Demo</a>
154
+ <a href="/demo/" class="btn btn-primary" style="text-decoration:none;text-align:center;">Try the Demo</a>
157
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>
158
157
  </div>
159
158
 
160
159
  <!-- Pair-approved view (CRC pair-mode, desktop-side after phone approves) -->
@@ -163,6 +162,15 @@ html, body {
163
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>
164
163
  </div>
165
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>
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>
172
+ </div>
173
+
166
174
  <div class="login-status" id="loginStatus" style="position:absolute;left:0;right:0;margin-top:16px;text-align:center;"></div>
167
175
  </div>
168
176
  </div>
@@ -234,12 +242,22 @@ function isSafariDesktop() {
234
242
  }
235
243
 
236
244
  function needsCustomQR() {
237
- return !isMobileDevice() && !isSafariDesktop() && !isLocalPasskeysOn();
245
+ return !isLocalPasskeysOn();
238
246
  }
239
247
 
240
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
+
241
259
  function isLocalPasskeysOn() {
242
- return localStorage.getItem('localPasskeys') === 'on';
260
+ return getLocalPasskeysPreference() === 'on';
243
261
  }
244
262
 
245
263
  function toggleLocalPasskeys() {
@@ -265,7 +283,7 @@ function updatePasskeysDot() {
265
283
 
266
284
  // ── `next` continuation carrier ──
267
285
  //
268
- // Two whitelisted next shapes:
286
+ // Three whitelisted next shapes:
269
287
  //
270
288
  // PAIR_NEXT_REGEX /pair/<CODE> using the daemon alphabet
271
289
  // (CODEX_PAIR_ALPHABET, length 6, L is
@@ -278,14 +296,18 @@ function updatePasskeysDot() {
278
296
  // desktop and mobile (this is navigation
279
297
  // continuation, not authority transfer).
280
298
  //
299
+ // DEMO_NEXT_REGEX /demo. Standard post-login continuation
300
+ // from the homepage CTA.
301
+ //
281
302
  // Anything else is silently dropped. `next` is NOT a general redirect
282
303
  // primitive. The server validates authoritatively; this client-side
283
304
  // check is defense-in-depth.
284
305
  var PAIR_NEXT_REGEX = /^\/pair\/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
285
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$/;
286
308
 
287
309
  function isWhitelistedNext(raw) {
288
- return typeof raw === 'string' && (PAIR_NEXT_REGEX.test(raw) || REMOTE_CONTROL_NEXT_REGEX.test(raw));
310
+ return typeof raw === 'string' && (PAIR_NEXT_REGEX.test(raw) || REMOTE_CONTROL_NEXT_REGEX.test(raw) || DEMO_NEXT_REGEX.test(raw));
289
311
  }
290
312
 
291
313
  function readPairNextFromQuery() {
@@ -367,7 +389,7 @@ function redirectToRemoteControlIfDirectLogin(result) {
367
389
  // authority rules from plan C6 + C8:
368
390
  //
369
391
  // - QR-scan path (approveResponse.next from the server): follow it for
370
- // either whitelisted shape. The phone scanned the desktop's QR,
392
+ // a whitelisted next. The phone scanned the desktop's QR,
371
393
  // signed in, and the server returned a sanitized `next`.
372
394
  //
373
395
  // - URL fallback path (no approveResponse.next):
@@ -385,10 +407,13 @@ function redirectToRemoteControlIfDirectLogin(result) {
385
407
  // rule because it's a navigation continuation, not authority transfer.
386
408
  //
387
409
  // Returns true if it redirected (caller should not run other view-switching).
388
- async function followPairNextIfPresent(approveResponse, identity) {
410
+ async function followPairNextIfPresent(approveResponse, identity, isNewAccount) {
389
411
  var next = null;
390
- // Path 1: QR phone-side approve. Server-blessed next, either shape.
412
+ // Path 1: QR phone-side approve. Server-blessed next.
391
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
+ }
392
417
  next = approveResponse.next;
393
418
  } else {
394
419
  // Path 2: URL ?next= fallback (no approveResponse). Branch by
@@ -396,7 +421,7 @@ async function followPairNextIfPresent(approveResponse, identity) {
396
421
  // restricted to mobile per C8.
397
422
  var urlNext = readPairNextFromQuery();
398
423
  if (urlNext) {
399
- if (REMOTE_CONTROL_NEXT_REGEX.test(urlNext)) {
424
+ if (REMOTE_CONTROL_NEXT_REGEX.test(urlNext) || DEMO_NEXT_REGEX.test(urlNext)) {
400
425
  next = urlNext;
401
426
  } else if (PAIR_NEXT_REGEX.test(urlNext) && isMobileDevice()) {
402
427
  next = urlNext;
@@ -418,17 +443,50 @@ async function followPairNextIfPresent(approveResponse, identity) {
418
443
  }
419
444
  sessionStorage.setItem('wip_api_key', identity.apiKey);
420
445
  if (identity.agentId) sessionStorage.setItem('wip_handle', identity.agentId);
446
+ storeDemoHandoffIfNeeded(next, identity, isNewAccount === true);
421
447
  location.replace(next);
422
448
  return true;
423
449
  }
424
450
 
425
451
  // ── QR Login (Chrome desktop fallback) ──
426
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
+ }
427
484
 
428
485
  async function startQrLogin(handle, mode) {
429
486
  var btn = document.getElementById('createBtn');
430
487
  btn.disabled = true;
431
488
  setStatus('', '');
489
+ qrLoginMode = mode || 'register';
432
490
 
433
491
  try {
434
492
  var pairNext = readPairNextFromQuery();
@@ -499,13 +557,15 @@ async function pollQrLogin(sessionId) {
499
557
  var urlNext = readPairNextFromQuery();
500
558
  if (urlNext && PAIR_NEXT_REGEX.test(urlNext)) {
501
559
  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.
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.
505
563
  sessionStorage.setItem('wip_api_key', data.apiKey);
506
564
  if (data.agentId) sessionStorage.setItem('wip_handle', data.agentId);
565
+ storeDemoHandoffIfNeeded(data.next, data, qrLoginMode === 'register');
507
566
  location.replace(data.next);
508
567
  } else {
568
+ storeDemoHandoffIfNeeded('/demo', data, qrLoginMode === 'register');
509
569
  document.getElementById('success-view').style.display = 'block';
510
570
  // Use credentialLabel (matches the saved-passkey label the user
511
571
  // sees in iOS Passwords / 1Password). Falls back to agentId for
@@ -523,6 +583,7 @@ function cancelQrLogin() {
523
583
  clearInterval(qrLoginPollTimer);
524
584
  qrLoginPollTimer = null;
525
585
  }
586
+ qrLoginMode = null;
526
587
  document.getElementById('qr-view').style.display = 'none';
527
588
  document.getElementById('signup-view').style.display = 'block';
528
589
  document.getElementById('createBtn').disabled = false;
@@ -594,7 +655,7 @@ async function doCreateAccount() {
594
655
  return;
595
656
  }
596
657
 
597
- // Chrome desktop without local passkeys: use custom QR code
658
+ // Local passkeys off means use WIP's QR cross-device login on any device.
598
659
  if (needsCustomQR() && !qrSessionMode) {
599
660
  startQrLogin(username);
600
661
  return;
@@ -672,6 +733,7 @@ async function doCreateAccount() {
672
733
  sessionId: qrSessionId,
673
734
  agentId: result.agentId,
674
735
  apiKey: result.apiKey,
736
+ tenantId: result.tenantId,
675
737
  credentialLabel: result.credentialLabel,
676
738
  }),
677
739
  });
@@ -682,13 +744,13 @@ async function doCreateAccount() {
682
744
  // CRC pair-mode: if the server returned a sanitized `next`, store
683
745
  // the api_key on the phone and redirect to /pair/<CODE>. Phone is
684
746
  // the actor that completes pair-complete (plan C6).
685
- if (await followPairNextIfPresent(approveResponse, result)) return;
747
+ if (await followPairNextIfPresent(approveResponse, result, true)) return;
748
+ storeDemoHandoffIfNeeded('/demo', result, true);
686
749
  document.getElementById('signup-view').style.display = 'none';
687
750
  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 the typed
690
- // username, then agentId, then "you".
691
- document.getElementById('welcome-name').textContent = result.credentialLabel || username || result.agentId || 'you';
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';
692
754
  setStatus('', '');
693
755
  } else {
694
756
  setStatus(result.error || 'Registration failed', 'error');
@@ -727,7 +789,7 @@ async function doSignIn() {
727
789
  return;
728
790
  }
729
791
 
730
- // Chrome desktop without local passkeys: use QR code for sign-in.
792
+ // Local passkeys off means use WIP's QR cross-device login on any device.
731
793
  // With local passkeys on, let Chrome show native dialog (supports YubiKey).
732
794
  if (needsCustomQR() && !qrSessionMode) {
733
795
  startQrLogin('', 'signin');
@@ -791,6 +853,7 @@ async function doSignIn() {
791
853
  sessionId: qrSessionId,
792
854
  agentId: result.agentId,
793
855
  apiKey: result.apiKey,
856
+ tenantId: result.tenantId,
794
857
  credentialLabel: result.credentialLabel,
795
858
  }),
796
859
  });
@@ -799,7 +862,8 @@ async function doSignIn() {
799
862
  // CRC pair-mode: if the server returned a sanitized `next`, store
800
863
  // the api_key on the phone and redirect to /pair/<CODE>. Phone is
801
864
  // the actor that completes pair-complete (plan C6).
802
- if (await followPairNextIfPresent(approveResponse, result)) return;
865
+ if (await followPairNextIfPresent(approveResponse, result, false)) return;
866
+ storeDemoHandoffIfNeeded('/demo', result, false);
803
867
  document.getElementById('signup-view').style.display = 'none';
804
868
  document.getElementById('success-view').style.display = 'block';
805
869
  // Use credentialLabel from the server, which matches the saved-
@@ -3,221 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <title>Privacy Policy - Kaleidoscope Demo</title>
7
- <style>
8
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
9
-
10
- body {
11
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
12
- background: #FFFDF5;
13
- color: #1a1a1a;
14
- -webkit-font-smoothing: antialiased;
15
- -webkit-text-size-adjust: 100%;
16
- line-height: 1.6;
17
- }
18
-
19
- .container {
20
- max-width: 980px;
21
- margin: 0 auto;
22
- padding: 16px 24px 80px;
23
- }
24
-
25
- .header {
26
- position: sticky;
27
- top: 0;
28
- z-index: 100;
29
- padding: calc(12px + env(safe-area-inset-top, 0px)) 20px 12px;
30
- background: rgba(255, 253, 245, 0.8);
31
- -webkit-backdrop-filter: saturate(180%) blur(20px);
32
- backdrop-filter: saturate(180%) blur(20px);
33
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
34
- display: flex;
35
- align-items: center;
36
- }
37
-
38
- .header a {
39
- display: flex;
40
- align-items: center;
41
- text-decoration: none;
42
- }
43
-
44
- h1 {
45
- font-size: 28px;
46
- font-weight: 700;
47
- letter-spacing: -0.02em;
48
- margin-bottom: 4px;
49
- }
50
-
51
- .subtitle {
52
- font-size: 17px;
53
- color: #8a8580;
54
- margin-bottom: 8px;
55
- }
56
-
57
- .updated {
58
- font-size: 13px;
59
- color: #b0aaa4;
60
- margin-bottom: 40px;
61
- }
62
-
63
- section {
64
- padding: 28px 0;
65
- border-top: 1px solid #e8e5de;
66
- }
67
-
68
- section:last-of-type {
69
- border-bottom: 1px solid #e8e5de;
70
- }
71
-
72
- h2 {
73
- font-size: 16px;
74
- font-weight: 600;
75
- letter-spacing: -0.01em;
76
- margin-bottom: 12px;
77
- }
78
-
79
- p {
80
- font-size: 15px;
81
- line-height: 1.65;
82
- color: #3a3a3a;
83
- margin-bottom: 12px;
84
- }
85
-
86
- p:last-child {
87
- margin-bottom: 0;
88
- }
89
-
90
- ul {
91
- list-style: none;
92
- padding: 0;
93
- margin-bottom: 12px;
94
- }
95
-
96
- ul li {
97
- font-size: 15px;
98
- line-height: 1.65;
99
- color: #3a3a3a;
100
- padding-left: 16px;
101
- position: relative;
102
- margin-bottom: 4px;
103
- }
104
-
105
- ul li::before {
106
- content: "\2022";
107
- position: absolute;
108
- left: 0;
109
- color: #b0aaa4;
110
- }
111
-
112
- a {
113
- color: #1a1a1a;
114
- }
115
-
116
- footer {
117
- margin-top: 48px;
118
- padding-bottom: 160px;
119
- }
120
- </style>
6
+ <meta http-equiv="refresh" content="0; url=/legal/privacy/">
7
+ <link rel="canonical" href="/legal/privacy/">
8
+ <title>Privacy Policy - WIP Computer</title>
121
9
  </head>
122
10
  <body>
123
- <div class="header">
124
- <a id="navIcon" href="/demo/"></a>
125
- </div>
126
- <div class="container">
127
-
128
- <h1>Privacy Policy</h1>
129
- <p class="subtitle">Kaleidoscope Demo</p>
130
- <p class="updated">Last updated: April 2, 2026</p>
131
-
132
- <section>
133
- <p>WIP Computer, Inc. ("we", "us") operates the Kaleidoscope demo at wip.computer/demo.</p>
134
- </section>
135
-
136
- <section>
137
- <h2>What We Collect</h2>
138
- <ul>
139
- <li>Passkey credential ID and public key (for authentication)</li>
140
- <li>Username (optional, if you provide one)</li>
141
- <li>Wallet balance (simulated, for demo purposes)</li>
142
- </ul>
143
- </section>
144
-
145
- <section>
146
- <h2>What We Do NOT Collect</h2>
147
- <ul>
148
- <li>Email addresses (unless voluntarily provided later)</li>
149
- <li>Passwords (passkeys only, no passwords exist)</li>
150
- <li>Biometric data (Face ID / fingerprint verification happens on your device, we never receive biometric data)</li>
151
- <li>Photos (camera captures are processed in your browser, never uploaded)</li>
152
- <li>Browsing history or tracking data</li>
153
- <li>Cookies (we use sessionStorage only, cleared when you close the browser)</li>
154
- </ul>
155
- </section>
156
-
157
- <section>
158
- <h2>How We Use Your Data</h2>
159
- <ul>
160
- <li>Authentication: verifying your passkey when you sign in</li>
161
- <li>Demo functionality: tracking simulated wallet balance</li>
162
- <li>Product improvement: aggregate usage patterns (no personal data)</li>
163
- </ul>
164
- </section>
165
-
166
- <section>
167
- <h2>Third-Party Services</h2>
168
- <ul>
169
- <li>xAI (Grok Imagine API): receives text prompts for image generation. Does not receive your photos or personal data.</li>
170
- <li>No analytics services</li>
171
- <li>No advertising networks</li>
172
- <li>No data brokers</li>
173
- </ul>
174
- </section>
175
-
176
- <section>
177
- <h2>Data Storage</h2>
178
- <p>Your passkey data is stored on our server at wip.computer. It is not encrypted at rest in this demo version. Production versions will implement end-to-end encryption where we architecturally cannot read your data.</p>
179
- </section>
180
-
181
- <section>
182
- <h2>Data Deletion</h2>
183
- <p>Contact <a href="mailto:hello@wip.computer">hello@wip.computer</a> to request deletion of your passkey data.</p>
184
- </section>
185
-
186
- <section>
187
- <h2>Your Rights</h2>
188
- <p>You can delete your passkey from your device at any time through your device settings (Settings > Passwords on iOS, chrome://settings/passwords on Chrome).</p>
189
- </section>
190
-
191
- <section>
192
- <h2>Changes</h2>
193
- <p>We may update this policy. Changes will be reflected in the "Last updated" date.</p>
194
- </section>
195
-
196
- <section>
197
- <h2>Contact</h2>
198
- <p><a href="mailto:hello@wip.computer">hello@wip.computer</a></p>
199
- </section>
200
-
201
- <footer>
202
- <div id="kscope-footer"></div>
203
- </footer>
204
- <script src="/demo/footer.js"></script>
205
- </div>
206
- <script>
207
- var SPRITE_COLS = 8, SPRITE_ROWS = 3, SPRITE_TOTAL = 24;
208
- var navIdx = Math.floor(Math.random() * SPRITE_TOTAL);
209
- function updateNavIcon() {
210
- var el = document.getElementById('navIcon');
211
- if (!el) return;
212
- var col = navIdx % SPRITE_COLS;
213
- var row = Math.floor(navIdx / SPRITE_COLS);
214
- var bgPosX = (col / (SPRITE_COLS - 1)) * 100;
215
- var bgPosY = (row / (SPRITE_ROWS - 1)) * 100;
216
- el.innerHTML = '<div style="width:28px;height:28px;overflow:hidden;"><div style="width:100%;height:100%;background:url(/demo/sprites.png);background-size:' + (SPRITE_COLS * 100) + '% ' + (SPRITE_ROWS * 100) + '%;background-position:' + bgPosX + '% ' + bgPosY + '%;"></div></div>';
217
- navIdx = (navIdx + 1) % SPRITE_TOTAL;
218
- }
219
- updateNavIcon();
220
- setInterval(updateNavIcon, 6000);
221
- </script>
11
+ <p>The WIP Computer Privacy Policy has moved to <a href="/legal/privacy/">/legal/privacy/</a>.</p>
222
12
  </body>
223
13
  </html>
@@ -3,196 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <title>Terms of Service - Kaleidoscope Demo</title>
7
- <style>
8
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
9
-
10
- body {
11
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
12
- background: #FFFDF5;
13
- color: #1a1a1a;
14
- -webkit-font-smoothing: antialiased;
15
- -webkit-text-size-adjust: 100%;
16
- line-height: 1.6;
17
- }
18
-
19
- .container {
20
- max-width: 980px;
21
- margin: 0 auto;
22
- padding: 16px 24px 80px;
23
- }
24
-
25
- .header {
26
- position: sticky;
27
- top: 0;
28
- z-index: 100;
29
- padding: calc(12px + env(safe-area-inset-top, 0px)) 20px 12px;
30
- background: rgba(255, 253, 245, 0.8);
31
- -webkit-backdrop-filter: saturate(180%) blur(20px);
32
- backdrop-filter: saturate(180%) blur(20px);
33
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
34
- display: flex;
35
- align-items: center;
36
- }
37
-
38
- .header a {
39
- display: flex;
40
- align-items: center;
41
- text-decoration: none;
42
- }
43
-
44
- h1 {
45
- font-size: 28px;
46
- font-weight: 700;
47
- letter-spacing: -0.02em;
48
- margin-bottom: 4px;
49
- }
50
-
51
- .subtitle {
52
- font-size: 17px;
53
- color: #8a8580;
54
- margin-bottom: 8px;
55
- }
56
-
57
- .updated {
58
- font-size: 13px;
59
- color: #b0aaa4;
60
- margin-bottom: 40px;
61
- }
62
-
63
- section {
64
- padding: 28px 0;
65
- border-top: 1px solid #e8e5de;
66
- }
67
-
68
- section:last-of-type {
69
- border-bottom: 1px solid #e8e5de;
70
- }
71
-
72
- h2 {
73
- font-size: 16px;
74
- font-weight: 600;
75
- letter-spacing: -0.01em;
76
- margin-bottom: 12px;
77
- }
78
-
79
- p {
80
- font-size: 15px;
81
- line-height: 1.65;
82
- color: #3a3a3a;
83
- margin-bottom: 12px;
84
- }
85
-
86
- p:last-child {
87
- margin-bottom: 0;
88
- }
89
-
90
- ul {
91
- list-style: none;
92
- padding: 0;
93
- margin-bottom: 12px;
94
- }
95
-
96
- ul li {
97
- font-size: 15px;
98
- line-height: 1.65;
99
- color: #3a3a3a;
100
- padding-left: 16px;
101
- position: relative;
102
- margin-bottom: 4px;
103
- }
104
-
105
- ul li::before {
106
- content: "\2022";
107
- position: absolute;
108
- left: 0;
109
- color: #b0aaa4;
110
- }
111
-
112
- a {
113
- color: #1a1a1a;
114
- }
115
-
116
- footer {
117
- margin-top: 48px;
118
- padding-bottom: 160px;
119
- }
120
- </style>
6
+ <meta http-equiv="refresh" content="0; url=/legal/internet-services/kaleidoscope/">
7
+ <link rel="canonical" href="/legal/internet-services/kaleidoscope/">
8
+ <title>Kaleidoscope Terms of Service - WIP Computer</title>
121
9
  </head>
122
10
  <body>
123
- <div class="header">
124
- <a id="navIcon" href="/demo/"></a>
125
- </div>
126
- <div class="container">
127
-
128
- <h1>Terms of Service</h1>
129
- <p class="subtitle">Kaleidoscope Demo</p>
130
- <p class="updated">Last updated: April 2, 2026</p>
131
-
132
- <section>
133
- <p>This is a technology demonstration by WIP Computer, Inc.</p>
134
- </section>
135
-
136
- <section>
137
- <h2>The Demo</h2>
138
- <p>This demo showcases passkey authentication, biometric permission, and AI image generation. It is not a production service.</p>
139
- </section>
140
-
141
- <section>
142
- <h2>Your Account</h2>
143
- <p>Your account is created with a passkey. No email or password is stored. Your passkey lives on your device.</p>
144
- </section>
145
-
146
- <section>
147
- <h2>Your Wallet</h2>
148
- <p>The demo wallet starts with $5.00 in simulated credits. No real money is charged. The wallet balance is for demonstration purposes only.</p>
149
- </section>
150
-
151
- <section>
152
- <h2>Generated Content</h2>
153
- <p>Images generated through this demo are created using third-party AI APIs (xAI Grok Imagine). Generated content is dual-licensed:</p>
154
- <ul>
155
- <li>MIT License for personal, non-commercial use</li>
156
- <li>Apache 2.0 License for commercial use</li>
157
- </ul>
158
- <p>Both you and WIP Computer, Inc. retain rights to generated content. We may use generated images for product development, marketing, and research.</p>
159
- </section>
160
-
161
- <section>
162
- <h2>Uploaded Media</h2>
163
- <p>Photos taken through the camera feature are processed locally in your browser. They are not uploaded to or stored on our servers. Camera access is optional and can be denied.</p>
164
- </section>
165
-
166
- <section>
167
- <h2>No Warranty</h2>
168
- <p>This demo is provided "as is" without warranty of any kind. WIP Computer, Inc. is not liable for any damages arising from use of this demo.</p>
169
- </section>
170
-
171
- <section>
172
- <h2>Contact</h2>
173
- <p><a href="mailto:hello@wip.computer">hello@wip.computer</a></p>
174
- </section>
175
-
176
- <footer>
177
- <div id="kscope-footer"></div>
178
- </footer>
179
- <script src="/demo/footer.js"></script>
180
- </div>
181
- <script>
182
- var SPRITE_COLS = 8, SPRITE_ROWS = 3, SPRITE_TOTAL = 24;
183
- var navIdx = Math.floor(Math.random() * SPRITE_TOTAL);
184
- function updateNavIcon() {
185
- var el = document.getElementById('navIcon');
186
- if (!el) return;
187
- var col = navIdx % SPRITE_COLS;
188
- var row = Math.floor(navIdx / SPRITE_COLS);
189
- var bgPosX = (col / (SPRITE_COLS - 1)) * 100;
190
- var bgPosY = (row / (SPRITE_ROWS - 1)) * 100;
191
- el.innerHTML = '<div style="width:28px;height:28px;overflow:hidden;"><div style="width:100%;height:100%;background:url(/demo/sprites.png);background-size:' + (SPRITE_COLS * 100) + '% ' + (SPRITE_ROWS * 100) + '%;background-position:' + bgPosX + '% ' + bgPosY + '%;"></div></div>';
192
- navIdx = (navIdx + 1) % SPRITE_TOTAL;
193
- }
194
- updateNavIcon();
195
- setInterval(updateNavIcon, 6000);
196
- </script>
11
+ <p>The Kaleidoscope Terms of Service have moved to <a href="/legal/internet-services/kaleidoscope/">/legal/internet-services/kaleidoscope/</a>.</p>
197
12
  </body>
198
13
  </html>