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