@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.
- package/bin/ldm.js +103 -0
- package/package.json +8 -2
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-pair-login-flow.mjs +76 -1
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
- package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
- package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
- package/shared/boot/boot-config.json +18 -8
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
- package/shared/rules/security.md +4 -0
- package/src/boot/README.md +24 -1
- package/src/boot/boot-config.json +18 -8
- package/src/boot/boot-hook.mjs +118 -28
- package/src/boot/installer.mjs +33 -10
- package/src/hosted-mcp/.env.example +4 -0
- package/src/hosted-mcp/app/footer.js +2 -2
- package/src/hosted-mcp/app/kaleidoscope-login.html +486 -42
- package/src/hosted-mcp/app/wip-logo.png +0 -0
- package/src/hosted-mcp/demo/footer.js +2 -2
- package/src/hosted-mcp/demo/index.html +166 -44
- package/src/hosted-mcp/demo/login.html +87 -23
- package/src/hosted-mcp/demo/privacy.html +4 -214
- package/src/hosted-mcp/demo/tos.html +4 -189
- package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
- package/src/hosted-mcp/legal/legal-footer.js +75 -0
- package/src/hosted-mcp/legal/legal.css +166 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
- package/src/hosted-mcp/legal/privacy/index.html +253 -0
- package/src/hosted-mcp/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:
|
|
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/"
|
|
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 !
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
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
|
-
//
|
|
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
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
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
|
-
<
|
|
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>
|