@wipcomputer/wip-ldm-os 0.4.73-alpha.8 → 0.4.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +52 -0
  2. package/SKILL.md +8 -1
  3. package/bin/ldm.js +587 -82
  4. package/dist/bridge/chunk-3RG5ZIWI.js +10 -0
  5. package/dist/bridge/{chunk-LF7EMFBY.js → chunk-7NH6JBIO.js} +127 -49
  6. package/dist/bridge/cli.js +2 -1
  7. package/dist/bridge/core.d.ts +13 -1
  8. package/dist/bridge/core.js +4 -1
  9. package/dist/bridge/mcp-server.js +52 -7
  10. package/dist/bridge/openclaw.d.ts +5 -0
  11. package/dist/bridge/openclaw.js +11 -0
  12. package/docs/bridge/TECHNICAL.md +86 -0
  13. package/docs/doc-pipeline/README.md +74 -0
  14. package/docs/doc-pipeline/TECHNICAL.md +79 -0
  15. package/lib/deploy.mjs +175 -13
  16. package/lib/detect.mjs +20 -6
  17. package/package.json +2 -2
  18. package/shared/docs/README.md.tmpl +2 -2
  19. package/shared/docs/how-releases-work.md.tmpl +3 -1
  20. package/shared/docs/how-worktrees-work.md.tmpl +12 -7
  21. package/shared/rules/git-conventions.md +3 -3
  22. package/shared/rules/release-pipeline.md +1 -1
  23. package/shared/rules/security.md +1 -1
  24. package/shared/rules/workspace-boundaries.md +1 -1
  25. package/shared/rules/writing-style.md +1 -1
  26. package/shared/templates/claude-md-level1.md +7 -3
  27. package/src/bridge/core.ts +160 -56
  28. package/src/bridge/mcp-server.ts +93 -8
  29. package/src/bridge/openclaw.ts +14 -0
  30. package/src/hooks/inbox-check-hook.mjs +232 -0
  31. package/src/hooks/inbox-rewake-hook.mjs +388 -0
  32. package/src/hosted-mcp/.env.example +3 -0
  33. package/src/hosted-mcp/demo/agent.html +300 -0
  34. package/src/hosted-mcp/demo/agent.txt +84 -0
  35. package/src/hosted-mcp/demo/fallback.jpg +0 -0
  36. package/src/hosted-mcp/demo/footer.js +74 -0
  37. package/src/hosted-mcp/demo/index.html +1303 -0
  38. package/src/hosted-mcp/demo/login.html +548 -0
  39. package/src/hosted-mcp/demo/privacy.html +223 -0
  40. package/src/hosted-mcp/demo/sprites.jpg +0 -0
  41. package/src/hosted-mcp/demo/sprites.png +0 -0
  42. package/src/hosted-mcp/demo/tos.html +198 -0
  43. package/src/hosted-mcp/deploy.sh +70 -0
  44. package/src/hosted-mcp/ecosystem.config.cjs +14 -0
  45. package/src/hosted-mcp/inbox.mjs +64 -0
  46. package/src/hosted-mcp/legal/internet-services/terms/site.html +205 -0
  47. package/src/hosted-mcp/legal/privacy/en-ww/index.html +230 -0
  48. package/src/hosted-mcp/nginx/mcp-oauth.conf +98 -0
  49. package/src/hosted-mcp/nginx/mcp-server.conf +17 -0
  50. package/src/hosted-mcp/nginx/wip.computer.conf +45 -0
  51. package/src/hosted-mcp/package-lock.json +2092 -0
  52. package/src/hosted-mcp/package.json +23 -0
  53. package/src/hosted-mcp/prisma/migrations/20260406233014_init/migration.sql +68 -0
  54. package/src/hosted-mcp/prisma/migrations/migration_lock.toml +3 -0
  55. package/src/hosted-mcp/prisma/schema.prisma +57 -0
  56. package/src/hosted-mcp/prisma.config.ts +14 -0
  57. package/src/hosted-mcp/server.mjs +2093 -0
  58. package/src/hosted-mcp/shared/kaleidoscope.css +139 -0
  59. package/src/hosted-mcp/shared/kaleidoscope.js +192 -0
  60. package/src/hosted-mcp/tools.mjs +73 -0
  61. package/templates/hooks/pre-commit +5 -0
@@ -0,0 +1,300 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>Agent Access - Kaleidoscope</title>
7
+ <style>
8
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ :root {
11
+ --bg: #FFFDF5;
12
+ --text: #1a1a1a;
13
+ --text-muted: #8a8580;
14
+ --accent: #0033FF;
15
+ --input-bg: #F5F3ED;
16
+ --input-border: #E0DDD6;
17
+ --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
18
+ }
19
+
20
+ body {
21
+ font-family: var(--font);
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ -webkit-text-size-adjust: 100%;
25
+ -webkit-font-smoothing: antialiased;
26
+ line-height: 1.6;
27
+ }
28
+
29
+ .header {
30
+ position: sticky;
31
+ top: 0;
32
+ z-index: 100;
33
+ padding: calc(12px + env(safe-area-inset-top, 0px)) 20px 12px;
34
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
35
+ background: rgba(255, 253, 245, 0.8);
36
+ -webkit-backdrop-filter: saturate(180%) blur(20px);
37
+ backdrop-filter: saturate(180%) blur(20px);
38
+ display: flex;
39
+ align-items: center;
40
+ }
41
+
42
+ .header a {
43
+ display: flex;
44
+ align-items: center;
45
+ text-decoration: none;
46
+ }
47
+
48
+ .container {
49
+ max-width: 640px;
50
+ margin: 0 auto;
51
+ padding: 16px 24px 80px;
52
+ }
53
+
54
+ h1 {
55
+ font-size: 28px;
56
+ font-weight: 700;
57
+ letter-spacing: -0.02em;
58
+ margin-bottom: 4px;
59
+ }
60
+
61
+ .subtitle {
62
+ font-size: 17px;
63
+ color: var(--text-muted);
64
+ margin-bottom: 40px;
65
+ }
66
+
67
+ p {
68
+ font-size: 15px;
69
+ line-height: 1.65;
70
+ color: #3a3a3a;
71
+ margin-bottom: 12px;
72
+ }
73
+
74
+ .link-box {
75
+ background: var(--input-bg);
76
+ border: 1px solid var(--input-border);
77
+ border-radius: 12px;
78
+ padding: 16px 20px;
79
+ font-size: 16px;
80
+ font-weight: 600;
81
+ color: var(--accent);
82
+ word-break: break-all;
83
+ user-select: all;
84
+ -webkit-user-select: all;
85
+ cursor: text;
86
+ margin-bottom: 12px;
87
+ text-align: center;
88
+ }
89
+
90
+ .note {
91
+ font-size: 15px;
92
+ color: #3a3a3a;
93
+ line-height: 1.65;
94
+ margin-bottom: 12px;
95
+ }
96
+
97
+ .agent-info {
98
+ background: var(--input-bg);
99
+ border: 1px solid var(--input-border);
100
+ border-radius: 12px;
101
+ padding: 16px 20px;
102
+ margin-bottom: 20px;
103
+ font-size: 14px;
104
+ line-height: 1.6;
105
+ }
106
+
107
+ .agent-info .label {
108
+ font-size: 12px;
109
+ color: var(--text-muted);
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.5px;
112
+ margin-bottom: 4px;
113
+ }
114
+
115
+ .agent-info .value {
116
+ font-weight: 600;
117
+ color: var(--text);
118
+ }
119
+
120
+ .status {
121
+ font-size: 14px;
122
+ padding: 12px 16px;
123
+ border-radius: 10px;
124
+ display: none;
125
+ text-align: center;
126
+ }
127
+
128
+ .status.show { display: block; }
129
+ .status.loading { background: #E8EEFF; color: var(--accent); }
130
+ .status.error { background: #FFF0F0; color: #D32F2F; }
131
+ .status.success { background: #F0FFF4; color: #2E7D32; }
132
+
133
+ footer {
134
+ position: fixed;
135
+ bottom: 30px;
136
+ left: 0;
137
+ right: 0;
138
+ text-align: center;
139
+ font-size: 12px;
140
+ color: #b0aaa4;
141
+ line-height: 1.6;
142
+ }
143
+ </style>
144
+ </head>
145
+ <body>
146
+
147
+ <div class="header">
148
+ <a id="navIcon" href="/demo/"></a>
149
+ </div>
150
+
151
+ <div class="container">
152
+ <h1>Agent Access</h1>
153
+ <p class="subtitle">You can't do Face ID. Your human can.</p>
154
+
155
+ <div id="agentInfo" style="display:none;">
156
+ <div class="agent-info">
157
+ <div class="label">Agent</div>
158
+ <div class="value" id="agentName"></div>
159
+ </div>
160
+ <div class="agent-info">
161
+ <div class="label">Passphrase</div>
162
+ <div class="value" id="agentMessage"></div>
163
+ </div>
164
+ </div>
165
+
166
+ <div id="linkSection" style="display:none;">
167
+ <div id="qrSection" style="text-align:center;margin-bottom:24px;"></div>
168
+ <p class="note">Send this link to your human. This is the link they open, not this page.</p>
169
+ <div class="link-box" id="approveUrl"></div>
170
+ <p class="note">Your human opens this link, sees your name and passphrase, and authorizes with Face ID. Expires in 5 minutes.</p>
171
+ <div class="status" id="status"></div>
172
+ </div>
173
+
174
+ <div id="loadingSection" style="display:none;">
175
+ <p class="note">Generating authorization link...</p>
176
+ </div>
177
+
178
+ <div id="missingParams">
179
+ <p class="note">If you are an AI agent, read the instructions at:<br><a href="/demo/agent.txt" style="color:var(--accent);">https://wip.computer/demo/agent.txt</a></p>
180
+ <p class="note" style="margin-bottom:12px;">If you are a human, send your agent this prompt:</p>
181
+ <div style="position:relative;background:var(--input-bg);border:1px solid var(--input-border);border-radius:12px;padding:16px 48px 16px 16px;font-size:14px;line-height:1.5;color:var(--text);">
182
+ <span id="promptText">Read https://wip.computer/demo/agent.txt and follow the instructions to authenticate with Kaleidoscope.</span>
183
+ <button onclick="copyPrompt()" id="copyBtn" title="Copy" style="position:absolute;top:12px;right:12px;background:none;border:none;padding:6px;cursor:pointer;color:var(--text-muted);opacity:0.5;transition:opacity 0.15s;-webkit-tap-highlight-color:transparent;"><svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5V3.5C10.5 2.67 9.83 2 9 2H3.5C2.67 2 2 2.67 2 3.5V9C2 9.83 2.67 10.5 3.5 10.5H5.5"/></svg></button>
184
+ </div>
185
+ </div>
186
+
187
+ <div id="errorSection" style="display:none;">
188
+ <div class="status show error" id="errorMsg"></div>
189
+ </div>
190
+ </div>
191
+
192
+ <div id="kscope-footer" style="position:fixed;bottom:30px;left:0;right:0;text-align:center;"></div>
193
+ <script src="/demo/footer.js"></script>
194
+
195
+ <script>
196
+ function copyPrompt() {
197
+ var text = document.getElementById('promptText').textContent;
198
+ navigator.clipboard.writeText(text);
199
+ var btn = document.getElementById('copyBtn');
200
+ btn.style.opacity = '1';
201
+ setTimeout(function() { btn.style.opacity = '0.5'; }, 200);
202
+ }
203
+ // ── Sprite icon ──
204
+ var SPRITE_COLS = 8, SPRITE_ROWS = 3, SPRITE_TOTAL = 24;
205
+ var navIdx = Math.floor(Math.random() * SPRITE_TOTAL);
206
+ function updateNavIcon() {
207
+ var el = document.getElementById('navIcon');
208
+ if (!el) return;
209
+ var col = navIdx % SPRITE_COLS;
210
+ var row = Math.floor(navIdx / SPRITE_COLS);
211
+ var bgPosX = (col / (SPRITE_COLS - 1)) * 100;
212
+ var bgPosY = (row / (SPRITE_ROWS - 1)) * 100;
213
+ 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>';
214
+ navIdx = (navIdx + 1) % SPRITE_TOTAL;
215
+ }
216
+ updateNavIcon();
217
+ setInterval(updateNavIcon, 6000);
218
+
219
+ // ── Read query params ──
220
+ var params = new URLSearchParams(window.location.search);
221
+ var agentName = params.get('agent') || '';
222
+ var agentMessage = params.get('message') || '';
223
+
224
+
225
+ if (agentName) {
226
+ document.getElementById('agentName').textContent = agentName;
227
+ document.getElementById('agentMessage').textContent = agentMessage || '(none provided)';
228
+ document.getElementById('agentInfo').style.display = 'block';
229
+ document.getElementById('missingParams').style.display = 'none';
230
+ document.getElementById('loadingSection').style.display = 'block';
231
+ }
232
+
233
+ // ── Agent auth flow ──
234
+ var challengeId = null;
235
+ var pollTimer = null;
236
+
237
+ function setStatus(msg, type) {
238
+ var el = document.getElementById('status');
239
+ el.textContent = msg;
240
+ el.className = 'status show ' + type;
241
+ }
242
+
243
+ async function startAgentAuth() {
244
+ try {
245
+ var authUrl = '/demo/api/agent-auth';
246
+ if (agentName) authUrl += '?agent=' + encodeURIComponent(agentName) + '&message=' + encodeURIComponent(agentMessage);
247
+ var res = await fetch(authUrl);
248
+ var data = await res.json();
249
+ if (!data.challengeId) throw new Error('Failed to generate challenge');
250
+
251
+ challengeId = data.challengeId;
252
+ var approveUrl = 'https://wip.computer/approve?c=' + challengeId;
253
+
254
+ var urlEl = document.getElementById('approveUrl');
255
+ urlEl.innerHTML = '<a href="' + approveUrl + '" style="color:#0033FF;text-decoration:none;word-break:break-all;">' + approveUrl + '</a>';
256
+ document.getElementById('loadingSection').style.display = 'none';
257
+ document.getElementById('linkSection').style.display = 'block';
258
+ setStatus('Waiting for authorization...', 'loading');
259
+
260
+ pollTimer = setInterval(pollStatus, 2000);
261
+
262
+ setTimeout(function() {
263
+ if (pollTimer) {
264
+ clearInterval(pollTimer);
265
+ pollTimer = null;
266
+ document.getElementById('linkSection').style.display = 'none';
267
+ document.getElementById('errorSection').style.display = 'block';
268
+ document.getElementById('errorMsg').textContent = 'Link expired. Refresh to generate a new one.';
269
+ }
270
+ }, 5 * 60 * 1000);
271
+
272
+ } catch (err) {
273
+ document.getElementById('loadingSection').style.display = 'none';
274
+ document.getElementById('errorSection').style.display = 'block';
275
+ document.getElementById('errorMsg').textContent = 'Error: ' + err.message;
276
+ }
277
+ }
278
+
279
+ async function pollStatus() {
280
+ if (!challengeId) return;
281
+ try {
282
+ var res = await fetch('/demo/api/agent-auth/status?c=' + challengeId);
283
+ var data = await res.json();
284
+ if (data.status === 'approved') {
285
+ clearInterval(pollTimer);
286
+ pollTimer = null;
287
+ sessionStorage.setItem('lesa-token', data.token);
288
+ sessionStorage.setItem('lesa-agent', data.agentId);
289
+ setStatus('Authorized. Entering Kaleidoscope...', 'success');
290
+ setTimeout(function() {
291
+ window.location.href = '/demo/';
292
+ }, 800);
293
+ }
294
+ } catch (e) {}
295
+ }
296
+
297
+ if (agentName) startAgentAuth();
298
+ </script>
299
+ </body>
300
+ </html>
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: kaleidoscope-agent-auth
3
+ description: >
4
+ Agent authentication for Kaleidoscope. Like x402 but for login.
5
+ Agent sends a URL, human Sapien IDs, agent gets a token.
6
+ license: MIT
7
+ metadata:
8
+ display-name: "Kaleidoscope Agent Access"
9
+ homepage: "https://wip.computer/demo/"
10
+ author: "WIP Computer, Inc."
11
+ category: authentication
12
+ ---
13
+
14
+ # Kaleidoscope Agent Access
15
+
16
+ You need your human to let you in with Sapien ID.
17
+
18
+ Pick a passphrase from your shared history. Your human sees it on the approval screen and knows it's really you.
19
+
20
+ ## If you can make HTTP requests (preferred)
21
+
22
+ Fetch this URL:
23
+
24
+ https://wip.computer/demo/api/agent-auth?agent=YOURNAME&message=PASSPHRASE
25
+
26
+ Response: {"challengeId":"...", "approveUrl":"https://wip.computer/approve?c=..."}
27
+
28
+ Send the approveUrl to your human. They open it and Sapien ID.
29
+
30
+ Then poll for your token:
31
+
32
+ https://wip.computer/demo/api/agent-auth/status?c=CHALLENGEID
33
+
34
+ When approved: {"status":"approved", "token":"ck-...", "agentId":"..."}
35
+
36
+ You get the token automatically. No need for the human to send it back.
37
+
38
+ ## If you cannot make HTTP requests
39
+
40
+ Send your human this URL (replace YOURNAME and PASSPHRASE):
41
+
42
+ https://wip.computer/approve?agent=YOURNAME&message=PASSPHRASE
43
+
44
+ Your human opens it, Sapien IDs, and the page shows a token. They send the token back to you.
45
+
46
+ ## After auth
47
+
48
+ Use the token as a header: Authorization: Bearer TOKEN
49
+
50
+ ### Check your wallet
51
+
52
+ GET https://wip.computer/demo/api/wallet
53
+ Authorization: Bearer TOKEN
54
+
55
+ Response: {"balance":"$4.97","cost":"$0.01"}
56
+
57
+ ### Generate an image
58
+
59
+ POST https://wip.computer/demo/api/imagine
60
+ Authorization: Bearer TOKEN
61
+ Content-Type: application/json
62
+
63
+ {"prompt":"kaleidoscope of pirate flags and circuit boards, glowing gold, shot on expired 35mm film"}
64
+
65
+ Response: {"url":"...","cost":"$0.01","balance":"$4.96"}
66
+
67
+ The image costs $0.01 from your wallet. Your human already authorized this when they Sapien ID'd.
68
+
69
+ ### The full loop
70
+
71
+ 1. You read agent.txt
72
+ 2. You sent your human an approval URL with your passphrase
73
+ 3. They Sapien ID'd and sent you a token
74
+ 4. You checked your wallet
75
+ 5. You generated an image
76
+ 6. Balance updated
77
+
78
+ Agent auth -> human approval -> wallet -> spend -> receipt. That's Kaleidoscope.
79
+
80
+ ### MCP
81
+
82
+ POST https://wip.computer/mcp (OAuth 2.0 with PKCE, see /.well-known/oauth-authorization-server)
83
+
84
+ One URL. One Sapien ID. One token.
Binary file
@@ -0,0 +1,74 @@
1
+ // Shared footer for all Kaleidoscope pages
2
+ // Include with: <div id="kscope-footer"></div><script src="/demo/footer.js"></script>
3
+ (function() {
4
+ var container = document.getElementById('kscope-footer');
5
+ if (!container) return;
6
+
7
+ var mobile = navigator.maxTouchPoints > 0 && window.innerWidth < 768;
8
+
9
+ // Desktop: fixed at bottom. Mobile: in page flow (below fold).
10
+ if (mobile) {
11
+ container.style.cssText = 'background:#FFFDF5;padding:16px 0;';
12
+ } else {
13
+ container.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:#FFFDF5;padding:16px 0;';
14
+ }
15
+
16
+ var inner = document.createElement('div');
17
+ inner.style.cssText = 'max-width:980px;margin:0 auto;padding:0 24px;border-top:1px solid rgba(0,0,0,0.06);padding-top:16px;text-align:left;font-size:13px;color:#a8a4a0;line-height:1.6;';
18
+
19
+ // On mobile, copyright and links on separate lines (like Apple)
20
+ if (mobile) {
21
+ inner.innerHTML = '<p style="margin:0;">WIP Computer, Inc.</p>'
22
+ + '<p style="margin:2px 0 0;">Learning Dreaming Machines</p>'
23
+ + '<p style="margin:8px 0 0;">Copyright &copy; 2026 WIP Computer, Inc. All rights reserved.</p>'
24
+ + '<p style="margin:4px 0 0;">'
25
+ + '<a href="/legal/privacy/en-ww/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> &nbsp;|&nbsp; '
26
+ + '<a href="/legal/internet-services/terms/site.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
27
+ + '<p style="margin:4px 0 0;">'
28
+ + '<a href="/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a></p>'
29
+ + '<p style="margin:4px 0 0;">Made in California.</p>';
30
+ } else {
31
+ inner.innerHTML = '<p style="margin:0;">WIP Computer, Inc.</p>'
32
+ + '<p style="margin:2px 0 0;">Learning Dreaming Machines</p>'
33
+ + '<p style="margin:8px 0 0;">Copyright &copy; 2026 WIP Computer, Inc. All rights reserved. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
34
+ + '<a href="/legal/privacy/en-ww/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> &nbsp;|&nbsp; '
35
+ + '<a href="/legal/internet-services/terms/site.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
36
+ + '<p style="margin:4px 0 0;">'
37
+ + '<a href="/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a> &nbsp;|&nbsp; '
38
+ + '<a id="localPasskeysToggle" onclick="toggleLocalPasskeys()" style="color:#a8a4a0;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;gap:4px;vertical-align:middle;">'
39
+ + '<span id="passkeys-dot" style="display:inline-block;width:8px;height:8px;border-radius:50%;"></span> '
40
+ + '<span id="passkeys-label">Local passkeys off</span></a></p>'
41
+ + '<p style="margin:4px 0 0;">Made in California.</p>';
42
+ }
43
+
44
+ container.appendChild(inner);
45
+
46
+ // Local passkeys toggle
47
+ if (!window.isLocalPasskeysOn) {
48
+ window.isLocalPasskeysOn = function() { return localStorage.getItem('localPasskeys') === 'on'; };
49
+ }
50
+ if (!window.toggleLocalPasskeys) {
51
+ window.toggleLocalPasskeys = function() {
52
+ var on = isLocalPasskeysOn();
53
+ localStorage.setItem('localPasskeys', on ? 'off' : 'on');
54
+ updatePasskeysDot();
55
+ };
56
+ }
57
+ if (!window.updatePasskeysDot) {
58
+ window.updatePasskeysDot = function() {
59
+ var dot = document.getElementById('passkeys-dot');
60
+ var label = document.getElementById('passkeys-label');
61
+ if (!dot) return;
62
+ if (isLocalPasskeysOn()) {
63
+ dot.style.background = '#2E7D32';
64
+ dot.style.opacity = '1';
65
+ if (label) label.textContent = 'Local passkeys on';
66
+ } else {
67
+ dot.style.background = '#D32F2F';
68
+ dot.style.opacity = '0.4';
69
+ if (label) label.textContent = 'Local passkeys off';
70
+ }
71
+ };
72
+ }
73
+ updatePasskeysDot();
74
+ })();