@wipcomputer/wip-ldm-os 0.4.85-alpha.2 → 0.4.85-alpha.21

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 (37) hide show
  1. package/README.md +22 -2
  2. package/SKILL.md +8 -5
  3. package/bin/ldm.js +169 -65
  4. package/docs/universal-installer/SPEC.md +16 -3
  5. package/docs/universal-installer/TECHNICAL.md +4 -4
  6. package/lib/deploy.mjs +104 -20
  7. package/lib/detect.mjs +35 -4
  8. package/package.json +13 -2
  9. package/scripts/test-crc-agentid-tenant-boundary.mjs +80 -0
  10. package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
  11. package/scripts/test-crc-e2ee-session-route.mjs +129 -0
  12. package/scripts/test-crc-pair-login-flow.mjs +40 -0
  13. package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
  14. package/scripts/test-crc-pair-status-poll-token.mjs +73 -0
  15. package/scripts/test-install-prompt-policy.mjs +60 -0
  16. package/scripts/test-installer-skill-directory.mjs +55 -0
  17. package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
  18. package/scripts/test-installer-target-self-update.mjs +131 -0
  19. package/scripts/test-ldm-status-timeout.mjs +80 -0
  20. package/shared/templates/install-prompt.md +20 -2
  21. package/src/hosted-mcp/README.md +15 -0
  22. package/src/hosted-mcp/app/footer.js +74 -0
  23. package/src/hosted-mcp/app/kaleidoscope-login.html +846 -0
  24. package/src/hosted-mcp/app/pair.html +165 -57
  25. package/src/hosted-mcp/app/sprites.png +0 -0
  26. package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
  27. package/src/hosted-mcp/demo/index.html +3 -7
  28. package/src/hosted-mcp/demo/login.html +318 -20
  29. package/src/hosted-mcp/deploy.sh +307 -56
  30. package/src/hosted-mcp/docs/self-host.md +268 -0
  31. package/src/hosted-mcp/nginx/codex-relay.conf +25 -0
  32. package/src/hosted-mcp/nginx/conf.d/redact-logs.conf +60 -0
  33. package/src/hosted-mcp/nginx/mcp-oauth.conf +58 -0
  34. package/src/hosted-mcp/nginx/wip.computer.conf +25 -1
  35. package/src/hosted-mcp/scripts/audit-logs.sh +205 -0
  36. package/src/hosted-mcp/scripts/verify-deploy.sh +102 -0
  37. package/src/hosted-mcp/server.mjs +963 -146
@@ -13,13 +13,21 @@
13
13
  --accent: #0033FF;
14
14
  --input-bg: #F5F3ED;
15
15
  --input-border: #E0DDD6;
16
+ --card-bg: #FFFFFF;
16
17
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
17
18
  }
18
19
  html, body { height: 100%; font-family: var(--font); background: var(--bg); color: var(--text); }
19
20
  .page { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
20
21
  .card { max-width: 420px; width: 100%; text-align: center; }
21
22
  h1 { font-size: 26px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; }
22
- .sub { font-size: 16px; color: var(--text-muted); margin-bottom: 32px; }
23
+ .sub { font-size: 16px; color: var(--text-muted); margin-bottom: 24px; }
24
+ .code-display {
25
+ font-family: ui-monospace, "SF Mono", monospace;
26
+ font-size: 28px; font-weight: 600; letter-spacing: 0.3em;
27
+ background: var(--card-bg); border: 1px solid var(--input-border);
28
+ border-radius: 12px; padding: 18px;
29
+ margin: 0 auto 20px; display: inline-block; min-width: 240px;
30
+ }
23
31
  input[type="text"] {
24
32
  width: 100%; padding: 18px; font-size: 22px; font-weight: 600;
25
33
  text-align: center; letter-spacing: 0.4em;
@@ -28,9 +36,10 @@
28
36
  text-transform: uppercase; font-family: ui-monospace, "SF Mono", monospace;
29
37
  }
30
38
  input[type="text"]:focus { border-color: var(--accent); outline: none; }
31
- .btn { display: block; width: 100%; padding: 18px; border: none; border-radius: 12px; font-size: 18px; font-weight: 600; font-family: var(--font); cursor: pointer; margin-top: 16px; }
39
+ .btn { display: block; width: 100%; padding: 18px; border: none; border-radius: 12px; font-size: 18px; font-weight: 600; font-family: var(--font); cursor: pointer; margin-top: 12px; }
32
40
  .btn:active { transform: scale(0.98); }
33
41
  .btn-primary { background: var(--accent); color: white; }
42
+ .btn-secondary { background: var(--card-bg); color: var(--text-muted); border: 1px solid var(--input-border); }
34
43
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
35
44
  .status { margin-top: 18px; font-size: 14px; min-height: 1.4em; color: var(--text-muted); }
36
45
  .status.error { color: #b00020; }
@@ -41,78 +50,177 @@
41
50
  <body>
42
51
  <div class="page">
43
52
  <div class="card">
44
- <h1>Pair your Mac</h1>
45
- <div class="sub">Type the code printed by <code>codex-daemon link</code></div>
46
- <input id="codeInput" type="text" maxlength="6" autocomplete="off" autocapitalize="characters" inputmode="text" placeholder="ABC123">
47
- <button id="pairBtn" class="btn btn-primary" type="button">Pair</button>
48
- <div id="status" class="status"></div>
49
- <div class="footer">Signed in as <span id="handle">...</span> ... <a href="#" id="signout">sign out</a></div>
53
+ <!-- View 1: confirm pair (when URL has a valid code + user is signed in) -->
54
+ <div id="confirm-view" style="display: none;">
55
+ <h1>Pair this laptop with Codex Remote Control?</h1>
56
+ <div class="sub">A device wants to drive its local Codex session from your phone.</div>
57
+ <div class="code-display" id="confirm-code">______</div>
58
+ <button id="confirmBtn" class="btn btn-primary" type="button">Confirm</button>
59
+ <button id="cancelBtn" class="btn btn-secondary" type="button">Cancel</button>
60
+ <div id="status" class="status"></div>
61
+ <div class="footer">Signed in as <span id="handle">...</span></div>
62
+ </div>
63
+
64
+ <!-- View 2: manual code entry (fallback for bare /pair) -->
65
+ <div id="manual-view" style="display: none;">
66
+ <h1>Pair your laptop</h1>
67
+ <div class="sub">Type the 6-character code printed by <code>codex-daemon link</code></div>
68
+ <input id="codeInput" type="text" maxlength="6" autocomplete="off" autocapitalize="characters" inputmode="text" placeholder="ABCDEF">
69
+ <button id="manualBtn" class="btn btn-primary" type="button">Pair</button>
70
+ <div id="manualStatus" class="status"></div>
71
+ <div class="footer">Signed in as <span id="manualHandle">...</span></div>
72
+ </div>
73
+
74
+ <!-- View 3: success -->
75
+ <div id="success-view" style="display: none;">
76
+ <h1>Paired.</h1>
77
+ <div id="success-sub" class="sub">Your laptop will pick this up in a few seconds.</div>
78
+ <div class="footer">You can close this tab.</div>
79
+ </div>
50
80
  </div>
51
81
  </div>
52
82
  <script>
53
- function getApiKey() {
54
- return sessionStorage.getItem("wip_api_key");
55
- }
56
- function getHandle() {
57
- return sessionStorage.getItem("wip_handle") || "(unknown)";
58
- }
59
- function setStatus(msg, kind) {
60
- const el = document.getElementById("status");
83
+ // Real daemon alphabet: A-Z minus I and O, digits 2-9. L IS included.
84
+ // Length 6. Per CODEX_PAIR_ALPHABET in server.mjs and plan C1+C3 round 5.
85
+ var PAIR_CODE_REGEX = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
86
+
87
+ function getApiKey() { return sessionStorage.getItem('wip_api_key'); }
88
+ function getHandle() { return sessionStorage.getItem('wip_handle') || '(unknown)'; }
89
+ function getPairPresenceToken() { return sessionStorage.getItem('wip_codex_pair_presence_token'); }
90
+
91
+ function setStatus(elId, msg, kind) {
92
+ var el = document.getElementById(elId);
61
93
  el.textContent = msg;
62
- el.className = "status" + (kind ? " " + kind : "");
63
- }
64
- function ensureSignedIn() {
65
- if (!getApiKey()) {
66
- location.href = "/app/login.html?next=" + encodeURIComponent("/pair");
67
- return false;
68
- }
69
- document.getElementById("handle").textContent = getHandle();
70
- return true;
94
+ el.className = 'status' + (kind ? ' ' + kind : '');
71
95
  }
72
96
 
73
- document.getElementById("signout").addEventListener("click", (ev) => {
74
- ev.preventDefault();
75
- sessionStorage.removeItem("wip_api_key");
76
- sessionStorage.removeItem("wip_handle");
77
- location.href = "/app/login.html?next=" + encodeURIComponent("/pair");
78
- });
97
+ // Read the code from /pair/<CODE>. Returns null if path is bare /pair or
98
+ // the code doesn't match the daemon alphabet. Defense-in-depth: server
99
+ // validates authoritatively via its route regex.
100
+ function readCodeFromPath() {
101
+ var m = location.pathname.match(/^\/pair\/([A-Za-z0-9]+)\/?$/);
102
+ if (!m) return null;
103
+ var raw = m[1].trim().toUpperCase();
104
+ return PAIR_CODE_REGEX.test(raw) ? raw : null;
105
+ }
79
106
 
80
- async function pair() {
81
- const input = document.getElementById("codeInput");
82
- const code = input.value.trim().toUpperCase();
83
- if (code.length !== 6) {
84
- setStatus("Enter the 6-character code shown on your Mac.", "error");
85
- return;
107
+ // POST the code to /api/codex-relay/pair-complete using the api_key
108
+ // from sessionStorage as Bearer token. Daemon key pairing also sends the
109
+ // short-lived token minted by the passkey verify step, so a stale ck- token
110
+ // alone cannot replace a daemon. On success, show the success view.
111
+ async function submitPair(code, statusElId) {
112
+ if (!PAIR_CODE_REGEX.test(code)) {
113
+ setStatus(statusElId, 'That does not look like a valid code.', 'error');
114
+ return false;
86
115
  }
87
- const btn = document.getElementById("pairBtn");
88
- btn.disabled = true;
89
- setStatus("Pairing...");
116
+ setStatus(statusElId, 'Pairing...', '');
90
117
  try {
91
- const res = await fetch("/api/codex-relay/pair-complete", {
92
- method: "POST",
118
+ var res = await fetch('/api/codex-relay/pair-complete', {
119
+ method: 'POST',
93
120
  headers: {
94
- "Content-Type": "application/json",
95
- "Authorization": "Bearer " + getApiKey(),
121
+ 'Content-Type': 'application/json',
122
+ 'Authorization': 'Bearer ' + getApiKey(),
96
123
  },
97
- body: JSON.stringify({ code }),
124
+ body: JSON.stringify({
125
+ code: code,
126
+ codex_pair_presence_token: getPairPresenceToken(),
127
+ }),
98
128
  });
99
- const data = await res.json();
100
- if (!res.ok) throw new Error(data.error || "Pairing failed");
101
- setStatus("Paired. Your Mac will pick this up in a few seconds.", "success");
102
- btn.textContent = "Done";
129
+ var data = await res.json();
130
+ if (!res.ok) {
131
+ var msg = (data && data.error) || 'Pairing failed.';
132
+ // If the code expired or was already used, suggest rerunning the daemon.
133
+ if (res.status === 410 || res.status === 404) {
134
+ msg += ' Run codex-daemon link again on your laptop to get a fresh code.';
135
+ }
136
+ if (res.status === 403 && data && data.error === 'fresh_presence_required') {
137
+ msg = 'Sign in again with your passkey to relink this daemon.';
138
+ sessionStorage.removeItem('wip_api_key');
139
+ sessionStorage.removeItem('wip_codex_pair_presence_token');
140
+ setTimeout(function() {
141
+ location.replace('/login?next=' + encodeURIComponent('/pair/' + code));
142
+ }, 1200);
143
+ }
144
+ setStatus(statusElId, msg, 'error');
145
+ return false;
146
+ }
147
+ sessionStorage.removeItem('wip_codex_pair_presence_token');
148
+ document.getElementById('success-sub').textContent = data.replaced_daemon_key
149
+ ? 'Remote Control relinked this laptop. Existing browser control tabs need to reconnect.'
150
+ : 'Your laptop will pick this up in a few seconds.';
151
+ document.getElementById('confirm-view').style.display = 'none';
152
+ document.getElementById('manual-view').style.display = 'none';
153
+ document.getElementById('success-view').style.display = 'block';
154
+ return true;
103
155
  } catch (err) {
104
- setStatus(err.message || String(err), "error");
105
- btn.disabled = false;
156
+ setStatus(statusElId, (err && err.message) || String(err), 'error');
157
+ return false;
106
158
  }
107
159
  }
108
160
 
109
- if (ensureSignedIn()) {
110
- document.getElementById("pairBtn").addEventListener("click", pair);
111
- document.getElementById("codeInput").addEventListener("keydown", (ev) => {
112
- if (ev.key === "Enter") pair();
161
+ // ── Routing ──
162
+ //
163
+ // 1. If not signed in: redirect to /login?next=<this-path-with-code>.
164
+ // The existing Kaleidoscope login flow will QR + passkey + redirect
165
+ // back here. The server's `next` whitelist also enforces this shape.
166
+ // 2. If signed in AND URL has a valid code: render the confirm view.
167
+ // User must tap Confirm before we POST pair-complete (per plan C7).
168
+ // 3. If signed in but no code in URL (bare /pair): render the manual
169
+ // entry view. Fallback only.
170
+
171
+ (function init() {
172
+ var code = readCodeFromPath();
173
+ var apiKey = getApiKey();
174
+
175
+ if (!apiKey) {
176
+ // Not signed in. Send through /login keeping the current path as next.
177
+ var nextParam = encodeURIComponent(location.pathname);
178
+ location.replace('/login?next=' + nextParam);
179
+ return;
180
+ }
181
+
182
+ if (code) {
183
+ // Signed in + URL has a valid code. Show confirm step.
184
+ var confirmView = document.getElementById('confirm-view');
185
+ document.getElementById('confirm-code').textContent = code;
186
+ document.getElementById('handle').textContent = getHandle();
187
+ confirmView.style.display = 'block';
188
+ document.getElementById('confirmBtn').addEventListener('click', async function() {
189
+ var confirmBtn = document.getElementById('confirmBtn');
190
+ var cancelBtn = document.getElementById('cancelBtn');
191
+ confirmBtn.disabled = true;
192
+ cancelBtn.disabled = true;
193
+ var ok = await submitPair(code, 'status');
194
+ // On failure, re-enable Confirm/Cancel so the user can retry or cancel.
195
+ // Server may have rejected the code as expired or already-used; the
196
+ // user should see the error and have the buttons back.
197
+ if (!ok) {
198
+ confirmBtn.disabled = false;
199
+ cancelBtn.disabled = false;
200
+ }
201
+ });
202
+ document.getElementById('cancelBtn').addEventListener('click', function() {
203
+ // Best-effort: just close the tab if we can; otherwise redirect home.
204
+ try { window.close(); } catch (e) {}
205
+ setTimeout(function() { location.replace('/'); }, 100);
206
+ });
207
+ return;
208
+ }
209
+
210
+ // Bare /pair, signed in. Manual code entry fallback.
211
+ var manualView = document.getElementById('manual-view');
212
+ document.getElementById('manualHandle').textContent = getHandle();
213
+ manualView.style.display = 'block';
214
+ function manualSubmit() {
215
+ var raw = document.getElementById('codeInput').value.trim().toUpperCase();
216
+ submitPair(raw, 'manualStatus');
217
+ }
218
+ document.getElementById('manualBtn').addEventListener('click', manualSubmit);
219
+ document.getElementById('codeInput').addEventListener('keydown', function(ev) {
220
+ if (ev.key === 'Enter') manualSubmit();
113
221
  });
114
- document.getElementById("codeInput").focus();
115
- }
222
+ document.getElementById('codeInput').focus();
223
+ })();
116
224
  </script>
117
225
  </body>
118
226
  </html>
Binary file
@@ -0,0 +1,208 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+
3
+ export function normalizeCodexCryptoVersions(versions) {
4
+ const out = Array.isArray(versions)
5
+ ? versions.filter((v) => typeof v === "string" && v.length > 0 && v.length <= 32).slice(0, 8)
6
+ : [];
7
+ return out.length ? out : ["e2ee-v1"];
8
+ }
9
+
10
+ export function codexDaemonPubkeyFingerprint(pubkey) {
11
+ if (typeof pubkey !== "string" || !pubkey) return null;
12
+ return "sha256:" + createHash("sha256").update(pubkey).digest("base64url").slice(0, 16);
13
+ }
14
+
15
+ export function evaluateCodexDaemonReconnectPubkey(existingKey, incomingPubkey) {
16
+ const existingPubkey = typeof existingKey?.pubkey === "string" && existingKey.pubkey ? existingKey.pubkey : null;
17
+ const nextPubkey = typeof incomingPubkey === "string" && incomingPubkey && incomingPubkey.length <= 1024 ? incomingPubkey : null;
18
+ const oldFingerprint = codexDaemonPubkeyFingerprint(existingPubkey);
19
+ const newFingerprint = codexDaemonPubkeyFingerprint(nextPubkey);
20
+
21
+ if (!nextPubkey) {
22
+ return {
23
+ allowed: false,
24
+ reason: "invalid_daemon_pubkey",
25
+ replaced: false,
26
+ old_fingerprint: oldFingerprint,
27
+ new_fingerprint: newFingerprint,
28
+ };
29
+ }
30
+ if (!existingPubkey || existingPubkey === nextPubkey) {
31
+ return {
32
+ allowed: true,
33
+ reason: null,
34
+ replaced: false,
35
+ old_fingerprint: oldFingerprint,
36
+ new_fingerprint: newFingerprint,
37
+ };
38
+ }
39
+ return {
40
+ allowed: false,
41
+ reason: "fresh_pair_required",
42
+ replaced: true,
43
+ old_fingerprint: oldFingerprint,
44
+ new_fingerprint: newFingerprint,
45
+ };
46
+ }
47
+
48
+ export function buildCodexBootstrapPayload({ identity, threadId, daemonOnline, daemonKey }) {
49
+ return {
50
+ handle: identity.handle,
51
+ thread_id: threadId,
52
+ daemon_online: daemonOnline,
53
+ daemon_public_key: daemonKey ? daemonKey.pubkey : null,
54
+ daemon_crypto_versions: daemonKey ? daemonKey.crypto_versions : null,
55
+ supported_crypto_versions: ["e2ee-v1"],
56
+ e2ee_available: !!daemonKey,
57
+ };
58
+ }
59
+
60
+ export function createCodexDaemonPubkeyRegistry({
61
+ usePrisma,
62
+ prisma,
63
+ devMode = false,
64
+ logger = console,
65
+ } = {}) {
66
+ const pubkeys = new Map();
67
+ const auditLog = [];
68
+
69
+ async function ensureStore() {
70
+ if (!usePrisma) return;
71
+ await prisma.$executeRawUnsafe(`
72
+ CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_keys (
73
+ tenant_id TEXT PRIMARY KEY,
74
+ pubkey TEXT NOT NULL,
75
+ crypto_versions_json TEXT NOT NULL,
76
+ registered_at TIMESTAMPTZ NOT NULL DEFAULT now()
77
+ )
78
+ `);
79
+ }
80
+
81
+ async function ensureAuditStore() {
82
+ if (!usePrisma) return;
83
+ await prisma.$executeRawUnsafe(`
84
+ CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_key_audit (
85
+ id TEXT PRIMARY KEY,
86
+ tenant_id TEXT NOT NULL,
87
+ source TEXT NOT NULL,
88
+ old_pubkey_fingerprint TEXT,
89
+ new_pubkey_fingerprint TEXT,
90
+ replaced BOOLEAN NOT NULL,
91
+ registered_at TIMESTAMPTZ NOT NULL DEFAULT now()
92
+ )
93
+ `);
94
+ }
95
+
96
+ async function loadFromDb() {
97
+ if (!usePrisma) return;
98
+ try {
99
+ await ensureStore();
100
+ const rows = await prisma.$queryRawUnsafe(`
101
+ SELECT tenant_id, pubkey, crypto_versions_json, registered_at
102
+ FROM codex_daemon_e2ee_keys
103
+ `);
104
+ for (const row of rows) {
105
+ let cryptoVersions = ["e2ee-v1"];
106
+ try { cryptoVersions = normalizeCodexCryptoVersions(JSON.parse(row.crypto_versions_json)); } catch {}
107
+ pubkeys.set(row.tenant_id, {
108
+ pubkey: row.pubkey,
109
+ crypto_versions: cryptoVersions,
110
+ registered_at: row.registered_at instanceof Date ? row.registered_at.toISOString() : String(row.registered_at),
111
+ });
112
+ }
113
+ logger.log("codex-relay: loaded " + rows.length + " persisted E2EE daemon pubkey(s)");
114
+ } catch (err) {
115
+ logger.error("codex-relay: failed to load persisted E2EE daemon pubkeys:", err.message);
116
+ if (!devMode) process.exit(1);
117
+ }
118
+ }
119
+
120
+ async function persist(agentId, pubkey, cryptoVersions) {
121
+ if (!usePrisma) return;
122
+ await ensureStore();
123
+ await prisma.$executeRawUnsafe(
124
+ `INSERT INTO codex_daemon_e2ee_keys
125
+ (tenant_id, pubkey, crypto_versions_json, registered_at)
126
+ VALUES ($1, $2, $3, now())
127
+ ON CONFLICT (tenant_id)
128
+ DO UPDATE SET
129
+ pubkey = EXCLUDED.pubkey,
130
+ crypto_versions_json = EXCLUDED.crypto_versions_json,
131
+ registered_at = EXCLUDED.registered_at`,
132
+ agentId,
133
+ pubkey,
134
+ JSON.stringify(cryptoVersions),
135
+ );
136
+ }
137
+
138
+ async function recordAudit(agentId, previousPubkey, nextPubkey, source, registeredAt) {
139
+ const oldFingerprint = codexDaemonPubkeyFingerprint(previousPubkey);
140
+ const newFingerprint = codexDaemonPubkeyFingerprint(nextPubkey);
141
+ const replaced = !!(previousPubkey && previousPubkey !== nextPubkey);
142
+ const entry = {
143
+ tenant_id: agentId,
144
+ source,
145
+ old_pubkey_fingerprint: oldFingerprint,
146
+ new_pubkey_fingerprint: newFingerprint,
147
+ replaced,
148
+ registered_at: registeredAt,
149
+ };
150
+ auditLog.push(entry);
151
+ if (!usePrisma) return;
152
+ await ensureAuditStore();
153
+ await prisma.$executeRawUnsafe(
154
+ `INSERT INTO codex_daemon_e2ee_key_audit
155
+ (id, tenant_id, source, old_pubkey_fingerprint, new_pubkey_fingerprint, replaced, registered_at)
156
+ VALUES ($1, $2, $3, $4, $5, $6, $7::timestamptz)`,
157
+ randomUUID(),
158
+ agentId,
159
+ source,
160
+ oldFingerprint,
161
+ newFingerprint,
162
+ replaced,
163
+ registeredAt,
164
+ );
165
+ }
166
+
167
+ function register(agentId, pubkey, cryptoVersions, source) {
168
+ if (typeof agentId !== "string" || !agentId) return Promise.resolve(false);
169
+ if (typeof pubkey !== "string" || !pubkey || pubkey.length > 1024) return Promise.resolve(false);
170
+ const previous = pubkeys.get(agentId) || null;
171
+ const normalizedVersions = normalizeCodexCryptoVersions(cryptoVersions);
172
+ const registeredAt = new Date().toISOString();
173
+ pubkeys.set(agentId, {
174
+ pubkey,
175
+ crypto_versions: normalizedVersions,
176
+ registered_at: registeredAt,
177
+ });
178
+ logger.log("codex-relay: registered E2EE pubkey for " + agentId + " via " + source);
179
+ return persist(agentId, pubkey, normalizedVersions)
180
+ .then(() => recordAudit(agentId, previous?.pubkey || null, pubkey, source, registeredAt))
181
+ .then(() => ({
182
+ registered: true,
183
+ replaced: !!(previous?.pubkey && previous.pubkey !== pubkey),
184
+ old_fingerprint: codexDaemonPubkeyFingerprint(previous?.pubkey || null),
185
+ new_fingerprint: codexDaemonPubkeyFingerprint(pubkey),
186
+ }))
187
+ .catch((err) => {
188
+ logger.error("codex-relay: failed to persist E2EE pubkey for " + agentId + ":", err.message);
189
+ if (!devMode) throw err;
190
+ return false;
191
+ });
192
+ }
193
+
194
+ return {
195
+ pubkeys,
196
+ auditLog,
197
+ ensureStore,
198
+ ensureAuditStore,
199
+ loadFromDb,
200
+ register,
201
+ get(agentId) {
202
+ return pubkeys.get(agentId) || null;
203
+ },
204
+ clearMemoryForTest() {
205
+ pubkeys.clear();
206
+ },
207
+ };
208
+ }
@@ -1233,13 +1233,9 @@ if (sessionStorage.getItem('lesa-token')) {
1233
1233
  showChat();
1234
1234
  }
1235
1235
 
1236
- // If user has an account (created at /login), show sign-in mode
1237
- if (localStorage.getItem('kscope-has-account') && !sessionStorage.getItem('lesa-token')) {
1238
- document.getElementById('createBtn').textContent = 'Enter the Kaleidoscope';
1239
- document.getElementById('createBtn').onclick = function() { doSignIn(); };
1240
- document.getElementById('handleInputWrap').style.display = 'none';
1241
- document.getElementById('signInBtn').parentElement.style.display = 'none';
1242
- }
1236
+ // /demo/ always shows the original create-account UI. Do not key off
1237
+ // localStorage["kscope-has-account"] to swap into sign-in mode here;
1238
+ // that coupled /login state to /demo/ rendering.
1243
1239
 
1244
1240
  // Handle send (not used in demo flow, but wired up)
1245
1241
  function handleSend() {