eleven / ocho Public

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>
Paul Campbell e13cfb0708bb671c606f8dc8ae81d77315f5d7b9 parent 32fdd11
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] }));