@wipcomputer/wip-ldm-os 0.4.85-alpha.9 → 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.
- package/README.md +22 -2
- package/SKILL.md +137 -15
- package/bin/ldm.js +608 -75
- package/lib/deploy.mjs +36 -1
- package/lib/registry-migrations.mjs +296 -0
- package/package.json +20 -3
- package/scripts/test-boot-dir-truth.mjs +158 -0
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +9 -2
- package/scripts/test-crc-pair-login-flow.mjs +76 -1
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
- package/scripts/test-deploy-hook-ownership.mjs +160 -0
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-install-prompt-policy.mjs +84 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
- package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
- package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +96 -0
- package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
- package/scripts/test-readme-install-prompt.mjs +66 -0
- package/shared/boot/boot-config.json +18 -8
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
- package/shared/rules/security.md +4 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/boot/README.md +24 -1
- package/src/boot/boot-config.json +18 -8
- package/src/boot/boot-hook.mjs +118 -28
- package/src/boot/installer.mjs +33 -10
- package/src/hosted-mcp/.env.example +4 -0
- package/src/hosted-mcp/README.md +37 -0
- package/src/hosted-mcp/app/footer.js +2 -2
- package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
- package/src/hosted-mcp/app/pair.html +21 -3
- package/src/hosted-mcp/app/wip-logo.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
- package/src/hosted-mcp/demo/footer.js +2 -2
- package/src/hosted-mcp/demo/index.html +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/deploy.sh +2 -0
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
- package/src/hosted-mcp/legal/legal-footer.js +75 -0
- package/src/hosted-mcp/legal/legal.css +166 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
- package/src/hosted-mcp/legal/privacy/index.html +253 -0
- package/src/hosted-mcp/server.mjs +920 -74
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@type": "Offer",
|
|
19
19
|
"price": "0",
|
|
20
20
|
"priceCurrency": "USD",
|
|
21
|
-
"description": "Demo with $
|
|
21
|
+
"description": "Demo with $10.00 simulated wallet"
|
|
22
22
|
},
|
|
23
23
|
"author": {
|
|
24
24
|
"@type": "Organization",
|
|
@@ -70,6 +70,10 @@ html, body {
|
|
|
70
70
|
-webkit-font-smoothing: antialiased;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
body.chat-active {
|
|
74
|
+
background: #FFFFFF;
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
@media (min-width: 768px) {
|
|
74
78
|
html, body { overflow: hidden; }
|
|
75
79
|
}
|
|
@@ -200,6 +204,7 @@ html, body {
|
|
|
200
204
|
bottom: 0;
|
|
201
205
|
max-width: 600px;
|
|
202
206
|
margin: 0 auto;
|
|
207
|
+
background: #FFFFFF;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
.chat-header {
|
|
@@ -210,7 +215,7 @@ html, body {
|
|
|
210
215
|
text-align: center;
|
|
211
216
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
212
217
|
flex-shrink: 0;
|
|
213
|
-
background: rgba(255,
|
|
218
|
+
background: rgba(255, 255, 255, 0.8);
|
|
214
219
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
215
220
|
backdrop-filter: saturate(180%) blur(20px);
|
|
216
221
|
}
|
|
@@ -287,6 +292,22 @@ html, body {
|
|
|
287
292
|
margin-top: 6px;
|
|
288
293
|
}
|
|
289
294
|
|
|
295
|
+
.bubble a,
|
|
296
|
+
.bubble .chat-link-button {
|
|
297
|
+
color: var(--accent);
|
|
298
|
+
font: inherit;
|
|
299
|
+
text-decoration: underline;
|
|
300
|
+
text-underline-offset: 2px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.bubble .chat-link-button {
|
|
304
|
+
display: inline;
|
|
305
|
+
padding: 0;
|
|
306
|
+
border: 0;
|
|
307
|
+
background: transparent;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
/* Camera preview circle */
|
|
291
312
|
.camera-container {
|
|
292
313
|
display: flex;
|
|
@@ -477,7 +498,7 @@ html, body {
|
|
|
477
498
|
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
|
478
499
|
border-top: 1px solid var(--input-border);
|
|
479
500
|
flex-shrink: 0;
|
|
480
|
-
background:
|
|
501
|
+
background: #FFFFFF;
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
.chat-input-row {
|
|
@@ -574,7 +595,7 @@ html, body {
|
|
|
574
595
|
<!-- ── CHAT PAGE ── -->
|
|
575
596
|
<div class="chat-page" id="chatPage">
|
|
576
597
|
<div class="chat-header" style="position:relative;">
|
|
577
|
-
<
|
|
598
|
+
<button id="kscopeIcon" type="button" onclick="logoutAndSignBackIn()" aria-label="Log out and sign back in" style="cursor:pointer;position:absolute;left:16px;top:50%;transform:translateY(-50%);display:flex;align-items:center;border:0;background:transparent;padding:0;"></button>
|
|
578
599
|
<div class="chat-header-name">Kaleidoscope</div>
|
|
579
600
|
<div class="chat-header-sub"></div>
|
|
580
601
|
</div>
|
|
@@ -593,6 +614,20 @@ html, body {
|
|
|
593
614
|
<canvas id="photoCanvas" style="display:none;"></canvas>
|
|
594
615
|
|
|
595
616
|
<script>
|
|
617
|
+
// Direct /demo entry is not the public start path. The login handoff writes
|
|
618
|
+
// lesa-token before replacing into /demo, so authenticated continuations pass.
|
|
619
|
+
(function gateDirectDemoEntry() {
|
|
620
|
+
var hasDemoToken = false;
|
|
621
|
+
try {
|
|
622
|
+
hasDemoToken = !!sessionStorage.getItem('lesa-token');
|
|
623
|
+
} catch (err) {
|
|
624
|
+
hasDemoToken = false;
|
|
625
|
+
}
|
|
626
|
+
if (!hasDemoToken) {
|
|
627
|
+
location.replace('/login?next=/demo');
|
|
628
|
+
}
|
|
629
|
+
})();
|
|
630
|
+
|
|
596
631
|
// ── WebAuthn Helpers ──
|
|
597
632
|
|
|
598
633
|
function b64urlToBytes(b64url) {
|
|
@@ -687,6 +722,7 @@ async function doCreateAccount() {
|
|
|
687
722
|
if (result.success) {
|
|
688
723
|
sessionStorage.setItem('lesa-token', result.apiKey);
|
|
689
724
|
sessionStorage.setItem('lesa-agent', result.agentId);
|
|
725
|
+
if (result.tenantId) sessionStorage.setItem('lesa-tenant', result.tenantId);
|
|
690
726
|
sessionStorage.setItem('lesa-new-account', 'true');
|
|
691
727
|
setLoginStatus('Account created.', 'success');
|
|
692
728
|
setTimeout(function() { showChat(); }, 400);
|
|
@@ -757,6 +793,7 @@ async function doSignIn() {
|
|
|
757
793
|
if (result.success) {
|
|
758
794
|
sessionStorage.setItem('lesa-token', result.apiKey);
|
|
759
795
|
sessionStorage.setItem('lesa-agent', result.agentId);
|
|
796
|
+
if (result.tenantId) sessionStorage.setItem('lesa-tenant', result.tenantId);
|
|
760
797
|
setLoginStatus('Signed in.', 'success');
|
|
761
798
|
setTimeout(function() { showChat(); }, 400);
|
|
762
799
|
} else {
|
|
@@ -867,7 +904,7 @@ function showAuthButton() {
|
|
|
867
904
|
btn.innerHTML = '<span style="filter:brightness(0) invert(1);"></span> Authorize';
|
|
868
905
|
btn.onclick = function() {
|
|
869
906
|
container.remove();
|
|
870
|
-
addMessage('
|
|
907
|
+
addMessage('Authorizing', 'user');
|
|
871
908
|
doFaceIDAuth();
|
|
872
909
|
};
|
|
873
910
|
container.appendChild(btn);
|
|
@@ -878,17 +915,52 @@ function showAuthButton() {
|
|
|
878
915
|
|
|
879
916
|
// ── Settings ──
|
|
880
917
|
|
|
881
|
-
var WALLET_BALANCE = "$
|
|
918
|
+
var WALLET_BALANCE = "$10.00";
|
|
882
919
|
var IMAGE_COST = "$0.01";
|
|
883
|
-
var IMAGE_BALANCE_AFTER = "$
|
|
920
|
+
var IMAGE_BALANCE_AFTER = "$9.99";
|
|
884
921
|
var IMAGE_API_NAME = "Grok Imagine";
|
|
885
922
|
var IMAGE_API_PROVIDER = "xAI";
|
|
923
|
+
var IS_RETURNING_USER_FLOW = false;
|
|
924
|
+
|
|
925
|
+
async function getWalletSummary() {
|
|
926
|
+
var walletBalance = WALLET_BALANCE;
|
|
927
|
+
var walletCost = IMAGE_COST;
|
|
928
|
+
try {
|
|
929
|
+
var token = sessionStorage.getItem('lesa-token');
|
|
930
|
+
if (!token) return { balance: walletBalance, cost: walletCost };
|
|
931
|
+
var wRes = await fetch('/demo/api/wallet', { headers: { 'Authorization': 'Bearer ' + token } });
|
|
932
|
+
var wData = await wRes.json();
|
|
933
|
+
if (wData.balance) walletBalance = wData.balance;
|
|
934
|
+
if (wData.cost) walletCost = wData.cost;
|
|
935
|
+
} catch {}
|
|
936
|
+
return { balance: walletBalance, cost: walletCost };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function currentAccountLabel() {
|
|
940
|
+
return sessionStorage.getItem('lesa-agent') || 'this account';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function canonicalAccountId(value) {
|
|
944
|
+
if (typeof value !== 'string') return null;
|
|
945
|
+
var trimmed = value.trim();
|
|
946
|
+
if (!trimmed) return null;
|
|
947
|
+
return trimmed.indexOf('acct:') === 0 ? trimmed.slice(5) : trimmed;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function isSameAccountAuthorization(result, activeToken, activeTenant) {
|
|
951
|
+
var sessionAccount = canonicalAccountId(activeTenant);
|
|
952
|
+
var resultAccount = canonicalAccountId(result && result.tenantId);
|
|
953
|
+
if (sessionAccount && resultAccount) {
|
|
954
|
+
return sessionAccount === resultAccount;
|
|
955
|
+
}
|
|
956
|
+
return !!(activeToken && result && result.apiKey === activeToken);
|
|
957
|
+
}
|
|
886
958
|
|
|
887
959
|
// ── Image Prompts ──
|
|
888
960
|
|
|
889
|
-
var PROMPT_PHOTO = "
|
|
961
|
+
var PROMPT_PHOTO = "Create an abstract kaleidoscope image from the colors, light, and shapes in the user's photo. Use mirrored radial symmetry, analog film grain, warm color bleed, soft exposure, organic imperfections, and no text.";
|
|
890
962
|
|
|
891
|
-
var PROMPT_NO_PHOTO = "abstract kaleidoscope
|
|
963
|
+
var PROMPT_NO_PHOTO = "Create an abstract kaleidoscope image with mirrored radial symmetry, analog film grain, warm color bleed, soft exposure, organic imperfections, and no text.";
|
|
892
964
|
|
|
893
965
|
// ── Demo Flow ──
|
|
894
966
|
|
|
@@ -896,16 +968,19 @@ async function startDemo() {
|
|
|
896
968
|
await delay(600);
|
|
897
969
|
var isNew = sessionStorage.getItem('lesa-new-account') === 'true';
|
|
898
970
|
sessionStorage.removeItem('lesa-new-account');
|
|
899
|
-
await lesaSays("Hi, I'm L\u0113sa. Welcome to Kaleidoscope.", 1200);
|
|
900
|
-
await delay(1000);
|
|
901
971
|
if (isNew) {
|
|
902
|
-
|
|
972
|
+
IS_RETURNING_USER_FLOW = false;
|
|
973
|
+
await lesaSays("Hi, I'm L\u0113sa. Welcome to Kaleidoscope.", 1200);
|
|
974
|
+
await delay(1000);
|
|
975
|
+
await lesaSays("You just created an account with a passkey. The passkey lives on your phone.", 1200);
|
|
976
|
+
await lesaSays("Going forward, you can use your phone to log into any Work in Progress Computer service.", 1500);
|
|
977
|
+
await lesaSays("Anytime I need your permission to do something, I'll ask, and you authorize with your fingerprint or face. No passwords. Ever.", 1500);
|
|
978
|
+
await lesaSays("Want to see it in action? I'll try to do something that costs money, and you decide whether to let me.", 1200);
|
|
903
979
|
} else {
|
|
904
|
-
|
|
980
|
+
IS_RETURNING_USER_FLOW = true;
|
|
981
|
+
await lesaSays("Hi, " + currentAccountLabel() + ". Welcome back to Kaleidoscope.", 1200);
|
|
982
|
+
await lesaSays("Do you want to try giving me permission with your device to create something that costs money?", 1200);
|
|
905
983
|
}
|
|
906
|
-
await lesaSays("Going forward, you can use this passkey to log into any Work in Progress Computer service.", 1500);
|
|
907
|
-
await lesaSays("And I can use it too. Anytime I need your permission to do something, I'll ask, and you authorize with your fingerprint or face. No passwords. Ever.", 1500);
|
|
908
|
-
await lesaSays("Want to see it in action? I'll try to do something that costs money, and you decide whether to let me.", 1200);
|
|
909
984
|
|
|
910
985
|
showChoiceButtons([
|
|
911
986
|
{ label: 'Yes, show me', primary: true, action: demoYesShowMe },
|
|
@@ -916,22 +991,16 @@ async function startDemo() {
|
|
|
916
991
|
// ── "Yes, show me" path ──
|
|
917
992
|
|
|
918
993
|
async function demoYesShowMe() {
|
|
919
|
-
var
|
|
920
|
-
|
|
921
|
-
try {
|
|
922
|
-
var token = sessionStorage.getItem('lesa-token');
|
|
923
|
-
var wRes = await fetch('/demo/api/wallet', { headers: { 'Authorization': 'Bearer ' + token } });
|
|
924
|
-
var wData = await wRes.json();
|
|
925
|
-
if (wData.balance) walletBalance = wData.balance;
|
|
926
|
-
if (wData.cost) walletCost = wData.cost;
|
|
927
|
-
} catch {}
|
|
928
|
-
await lesaSays("I have a wallet with " + walletBalance + ". Do I have your permission to spend " + walletCost + " on image generation using the " + IMAGE_API_PROVIDER + " " + IMAGE_API_NAME + " API?", 1000);
|
|
994
|
+
var wallet = await getWalletSummary();
|
|
995
|
+
await lesaSays("I have a wallet with " + wallet.balance + ". Do I have your permission to spend " + wallet.cost + " on image generation using the " + IMAGE_API_PROVIDER + " " + IMAGE_API_NAME + " API?", 1000);
|
|
929
996
|
|
|
930
997
|
showAuthButton();
|
|
931
998
|
}
|
|
932
999
|
|
|
933
1000
|
async function doFaceIDAuth() {
|
|
934
1001
|
showTyping();
|
|
1002
|
+
var activeToken = sessionStorage.getItem('lesa-token');
|
|
1003
|
+
var activeTenant = sessionStorage.getItem('lesa-tenant');
|
|
935
1004
|
|
|
936
1005
|
try {
|
|
937
1006
|
var optRes = await fetch('/webauthn/auth-options', {
|
|
@@ -983,6 +1052,13 @@ async function doFaceIDAuth() {
|
|
|
983
1052
|
return;
|
|
984
1053
|
}
|
|
985
1054
|
|
|
1055
|
+
if (!isSameAccountAuthorization(result, activeToken, activeTenant)) {
|
|
1056
|
+
removeTyping();
|
|
1057
|
+
await lesaSays("That passkey belongs to a different account. This onboarding session is using " + currentAccountLabel() + ". Restart if you want to switch accounts.", 600);
|
|
1058
|
+
showAuthButton();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
986
1062
|
removeTyping();
|
|
987
1063
|
await lesaSays("Thanks for authorizing. Let me show you what I can do.", 600);
|
|
988
1064
|
await delay(500);
|
|
@@ -1081,13 +1157,32 @@ async function demoJustMakeOne() {
|
|
|
1081
1157
|
// ── "No thanks" path (skip permissions entirely) ──
|
|
1082
1158
|
|
|
1083
1159
|
async function demoNoThanks() {
|
|
1084
|
-
await
|
|
1085
|
-
|
|
1160
|
+
await showNoThanksFlow();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function showNoThanksFlow() {
|
|
1164
|
+
if (IS_RETURNING_USER_FLOW) {
|
|
1165
|
+
await showReturningUserNoThanksFlow();
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
await lesaSays("No problem. Let me show you something beautiful.", 1000);
|
|
1169
|
+
await generateKaleidoscope(false, { showWalletReceipt: false, showOutro: false });
|
|
1170
|
+
await showApprovedOutro();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function showReturningUserNoThanksFlow() {
|
|
1174
|
+
await lesaSays("No problem. You can come back anytime.", 1000);
|
|
1175
|
+
await lesaSays("At Work in Progress Computer, we are building the future of AI and human interaction.", 1500);
|
|
1176
|
+
await lesaSaysHTML('We look forward to providing you a streamlined experience across all the AIs you use. Follow us on X <a href="https://x.com/wipcomputer" target="_blank" rel="noopener">@wipcomputer</a> to keep up to date on when we release new features.', 1800);
|
|
1177
|
+
await lesaSays("Made in California by WIP Computer, Inc. Learning Dreaming Machines.", 1200);
|
|
1086
1178
|
}
|
|
1087
1179
|
|
|
1088
1180
|
// ── Image generation ──
|
|
1089
1181
|
|
|
1090
|
-
async function generateKaleidoscope(isPhotoPath) {
|
|
1182
|
+
async function generateKaleidoscope(isPhotoPath, options) {
|
|
1183
|
+
options = options || {};
|
|
1184
|
+
var showWalletReceipt = options.showWalletReceipt !== false;
|
|
1185
|
+
var showDefaultOutro = options.showOutro !== false;
|
|
1091
1186
|
var prompt = isPhotoPath ? PROMPT_PHOTO : PROMPT_NO_PHOTO;
|
|
1092
1187
|
|
|
1093
1188
|
// If photo path, analyze the photo with vision to extract colors/mood
|
|
@@ -1104,7 +1199,7 @@ async function generateKaleidoscope(isPhotoPath) {
|
|
|
1104
1199
|
});
|
|
1105
1200
|
var analyzeData = await analyzeRes.json();
|
|
1106
1201
|
if (analyzeData.description) {
|
|
1107
|
-
prompt = "abstract kaleidoscope
|
|
1202
|
+
prompt = "Create an abstract kaleidoscope image from these visual details in the user's photo: " + analyzeData.description + ". Use mirrored radial symmetry, analog film grain, warm color bleed, soft exposure, organic imperfections, and no text.";
|
|
1108
1203
|
}
|
|
1109
1204
|
} catch (e) {
|
|
1110
1205
|
// Vision analysis failed... fall back to original PROMPT_PHOTO
|
|
@@ -1146,10 +1241,12 @@ async function generateKaleidoscope(isPhotoPath) {
|
|
|
1146
1241
|
genBubble.innerHTML = '<img class="kaleidoscope-img" src="' + data.url + '" alt="Your kaleidoscope">';
|
|
1147
1242
|
scrollToBottom();
|
|
1148
1243
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1244
|
+
if (showWalletReceipt) {
|
|
1245
|
+
var cost = data.cost || IMAGE_COST;
|
|
1246
|
+
var balance = data.balance || IMAGE_BALANCE_AFTER;
|
|
1247
|
+
await lesaSays("Cost: " + cost + ". Balance: " + balance + ".", 800);
|
|
1248
|
+
}
|
|
1249
|
+
if (showDefaultOutro) await showOutro();
|
|
1153
1250
|
|
|
1154
1251
|
} catch (err) {
|
|
1155
1252
|
loadingDiv.remove();
|
|
@@ -1161,14 +1258,36 @@ async function generateKaleidoscope(isPhotoPath) {
|
|
|
1161
1258
|
}
|
|
1162
1259
|
|
|
1163
1260
|
async function showOutro() {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
await
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1261
|
+
if (IS_RETURNING_USER_FLOW) {
|
|
1262
|
+
await showReturningUserOutro();
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
await showApprovedOutro();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function showApprovedOutro() {
|
|
1269
|
+
await lesaSays("At Work in Progress Computer, we are building the future of AI and human interaction.", 1500);
|
|
1270
|
+
await lesaSays("We believe permission is a conversation. Your AI asks. You decide. One glance, one tap.", 1800);
|
|
1271
|
+
await lesaSaysHTML('To see how passkeys work across devices, open <a href="https://wip.computer/demo" target="_blank" rel="noopener">https://wip.computer/demo</a> in any web browser on any computer and use your phone to authenticate.', 1800);
|
|
1272
|
+
await lesaSays("You can also click the icon at the top of this chat to log out, and then you can immediately log back in with your phone too.", 1800);
|
|
1273
|
+
await lesaSaysHTML('We look forward to providing you a streamlined experience across all the AIs you use. Follow us on X <a href="https://x.com/wipcomputer" target="_blank" rel="noopener">@wipcomputer</a> to keep up to date on when we release new features.', 1800);
|
|
1274
|
+
await lesaSays("Your passkey will keep working after you leave.", 1200);
|
|
1275
|
+
await lesaSays("Made in California by WIP Computer, Inc. Learning Dreaming Machines.", 1200);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function showReturningUserOutro() {
|
|
1279
|
+
await lesaSays("At Work in Progress Computer, we are building the future of AI and human interaction.", 1500);
|
|
1280
|
+
await lesaSays("We believe permission is a conversation. Your AI asks. You decide. One glance, one tap.", 1800);
|
|
1281
|
+
await lesaSaysHTML('We look forward to providing you a streamlined experience across all the AIs you use. Follow us on X <a href="https://x.com/wipcomputer" target="_blank" rel="noopener">@wipcomputer</a> to keep up to date on when we release new features.', 1800);
|
|
1282
|
+
await lesaSays("Made in California by WIP Computer, Inc. Learning Dreaming Machines.", 1200);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function logoutAndSignBackIn() {
|
|
1286
|
+
sessionStorage.removeItem('lesa-token');
|
|
1287
|
+
sessionStorage.removeItem('lesa-agent');
|
|
1288
|
+
sessionStorage.removeItem('lesa-tenant');
|
|
1289
|
+
sessionStorage.removeItem('lesa-new-account');
|
|
1290
|
+
window.location.href = '/login?next=/demo';
|
|
1172
1291
|
}
|
|
1173
1292
|
|
|
1174
1293
|
async function doGameAuth() {
|
|
@@ -1191,7 +1310,7 @@ async function doGameAuth() {
|
|
|
1191
1310
|
removeTyping();
|
|
1192
1311
|
await lesaSays("You are not authorized to play war games with me.", 1200);
|
|
1193
1312
|
}
|
|
1194
|
-
await lesaSays("
|
|
1313
|
+
await lesaSays("Your passkey will continue to work.", 1500);
|
|
1195
1314
|
}
|
|
1196
1315
|
|
|
1197
1316
|
function showAuthButtonForGame() {
|
|
@@ -1203,7 +1322,7 @@ function showAuthButtonForGame() {
|
|
|
1203
1322
|
btn.innerHTML = '<span style="filter:brightness(0) invert(1);"></span> Authorize';
|
|
1204
1323
|
btn.onclick = function() {
|
|
1205
1324
|
container.remove();
|
|
1206
|
-
addMessage('
|
|
1325
|
+
addMessage('Authorizing', 'user');
|
|
1207
1326
|
doGameAuth();
|
|
1208
1327
|
};
|
|
1209
1328
|
container.appendChild(btn);
|
|
@@ -1223,6 +1342,9 @@ function showCSSKaleidoscope(bubble) {
|
|
|
1223
1342
|
|
|
1224
1343
|
function showChat() {
|
|
1225
1344
|
document.getElementById('loginPage').style.display = 'none';
|
|
1345
|
+
document.body.classList.add('chat-active');
|
|
1346
|
+
var footer = document.getElementById('kscope-footer');
|
|
1347
|
+
if (footer) footer.style.display = 'none';
|
|
1226
1348
|
var chatPage = document.getElementById('chatPage');
|
|
1227
1349
|
chatPage.style.display = 'flex';
|
|
1228
1350
|
startDemo();
|
|
@@ -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-
|