agent: Eleven greets new members by their (now-encrypted) name (#103)
Member display names are encrypted under the instance key now, so Eleven's welcome DM — which read the plaintext roster label — greeted "Hi , welcome!". Fetch + unwrap the instance key and decrypt the member's name blob; fall back to a plaintext label (bots keep one) or a nameless "Hi there, welcome!" when the key or the name isn't available yet.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+32
−6
agent/eleven.mjs+32 −6
diff --git a/agent/eleven.mjs b/agent/eleven.mjs| index 6c7b983..8c4a96d 100644 |
| --- a/agent/eleven.mjs |
| +++ b/agent/eleven.mjs |
| @@ -16,7 +16,7 @@ |
| // the sidecar, agent.mjs calls startWelcomer() + runBot() with these exports. |
| import fs from "node:fs"; |
| import { fileURLToPath } from "node:url"; |
| -import { importKey, encrypt, generateKey, wrapThreadKey, unwrapThreadKey } from "../web/static/crypto.js"; |
| +import { importKey, encrypt, decrypt, generateKey, wrapThreadKey, unwrapThreadKey } from "../web/static/crypto.js"; |
| import { BASE, requireBase } from "./config.mjs"; |
| import { provision } from "./provision.mjs"; |
| @@ -71,14 +71,38 @@ export async function loadElevenSession() { |
| return session; |
| } |
| -const welcomeText = (label) => |
| - `Hi ${label}, welcome! 👋 I'm ${ELEVEN_NAME}, your guide to this app.\n\n` + |
| +const welcomeText = (name) => |
| + `Hi ${name || "there"}, welcome! 👋 I'm ${ELEVEN_NAME}, your guide to this app.\n\n` + |
| `A few things worth knowing:\n` + |
| `• Everything here is end-to-end encrypted — only the people in a chat can read it.\n` + |
| `• Tap ➕ to start a chat or a group. Helpers (like me) have their own section there.\n` + |
| `• Double-tap a message to react, swipe right to reply.\n\n` + |
| `Ask me anything about how the app works — I'm happy to help!`; |
| +// Member display names are encrypted under the instance key (the server is |
| +// blind to them). Fetch + unwrap our copy; null if a holder hasn't granted it to |
| +// us yet (then we greet without a name rather than block). |
| +async function loadInstanceKey(session, get) { |
| + try { |
| + const r = await get("/api/instance-key"); |
| + if (!r || !r.wrapped) return null; |
| + return await importKey(await unwrapThreadKey(r.wrapped, session.userPriv)); |
| + } catch { |
| + return null; |
| + } |
| +} |
| + |
| +// A member's display name: decrypt the instance-key blob when we can, else fall |
| +// back to any plaintext label (bots keep one), else "" → a nameless greeting. |
| +async function memberName(person, instanceKey) { |
| + if (person.label_blob && instanceKey) { |
| + try { |
| + return await decrypt(instanceKey, person.label_blob); |
| + } catch {} |
| + } |
| + return person.label || ""; |
| +} |
| + |
| // One welcome pass: look for joined members we haven't welcomed, DM them. |
| // Returns false when Eleven is disabled (no session/invite). |
| export async function welcomeTick() { |
| @@ -89,6 +113,7 @@ export async function welcomeTick() { |
| r.json() |
| ); |
| + const instanceKey = await loadInstanceKey(session, get); |
| const { roster = [] } = await get("/api/roster"); |
| // People only (never other bots), joined only (a pending invitee has no key |
| // to wrap a DM to — they're picked up on a later tick, right after they join). |
| @@ -135,7 +160,8 @@ export async function welcomeTick() { |
| if (!r.thread_id) throw new Error("createThread failed"); |
| dm = { id: r.thread_id }; |
| } |
| - const blob = await encrypt(key, { name: ELEVEN_NAME, text: welcomeText(person.label), ts: Date.now() }); |
| + const name = await memberName(person, instanceKey); |
| + const blob = await encrypt(key, { name: ELEVEN_NAME, text: welcomeText(name), ts: Date.now() }); |
| const pr = await fetch(`${BASE}/api/messages?token=${encodeURIComponent(session.token)}`, { |
| method: "POST", |
| headers: { "content-type": "application/json" }, |
| @@ -143,9 +169,9 @@ export async function welcomeTick() { |
| }); |
| if (!pr.ok) throw new Error(`post ${pr.status}`); |
| welcomed.add(person.user_id); |
| - log(`welcomed ${person.label}`); |
| + log(`welcomed ${name || person.user_id}`); |
| } catch (e) { |
| - log(`welcome ${person.label}: ${e.message}`); // retried next tick (not marked welcomed) |
| + log(`welcome ${person.user_id}: ${e.message}`); // retried next tick (not marked welcomed) |
| } |
| } |
| fs.writeFileSync(WELCOMED_PATH, JSON.stringify({ welcomed: [...welcomed] })); |