GUIDES
Identify users
Two modes: open identity for personalization, signed identity for anything private. Signed identity is what gates uploads, the vault, and per-user tasks.
Every request the widget makes carries a user context (who is asking). EERRAA can either trust it as-is, or require it to be cryptographically signed by your server. The difference decides whether a user can be personalized, or actually trusted with private data.
Open identity
In open mode you set window.ERAConfig.user in the browser and EERRAA trusts it as written. It is the fastest way to personalize replies: the agent knows the name, role, and anydata you attach. But because it lives in the browser, a determined user could edit it, so open mode never unlocks anything sensitive.
<script>
window.ERAConfig = {
user: {
id: "user_123",
name: "Sara",
role: "admin",
data: { plan: "pro" }
}
}
</script>Use open identity when the stakes are cosmetic: greeting the user by name, tailoring tone, or feeding read-only context into policies.
Signed identity
Signed mode makes the identity tamper-proof. Your server signs the user context with a secret only it knows, and EERRAA verifies that signature on every request. This is what gates the parts of the platform where trust matters:
- Private knowledge uploads (a user reading only their own documents)
- The per-user credential vault
- Per-user MCP OAuth connections
- The My-tasks panel and scheduled tasks
VITE_ variable, not in a NEXT_PUBLIC_ variable, not in any bundle a user can read.The signing recipe
Sign on your server, then hand the signed context to the browser. The signature is an HMAC-SHA256 over a canonical (stably stringified) JSON of the identity plus a fresh Unix timestamp _ts. Sorting keys matters: EERRAA re-stringifies the same way to verify, so the field order must be deterministic.
import { createHmac } from "crypto"
function stableStringify(v) {
if (Array.isArray(v)) return "[" + v.map(stableStringify).join(",") + "]"
if (v && typeof v === "object") {
return "{" + Object.keys(v).sort()
.map(k => JSON.stringify(k) + ":" + stableStringify(v[k]))
.join(",") + "}"
}
return JSON.stringify(v)
}
function signUserContext(identity, secret) {
const ctx = Object.assign({}, identity, { _ts: Math.floor(Date.now() / 1000) })
ctx._sig = createHmac("sha256", secret).update(stableStringify(ctx)).digest("hex")
return ctx // identity plus _ts and _sig, valid for 5 minutes
}Return that object to your page and drop it straight into ERAConfig.user. The two extra fields, _ts and _sig, are what EERRAA checks.
// Server-side (e.g. an endpoint your page calls on load)
const signed = signUserContext(
{ id: "user_123", name: "Sara", role: "admin", data: { plan: "pro" } },
process.env.ERA_SIGNING_SECRET
)
res.json(signed)
// Browser: use the signed context verbatim
window.ERAConfig = { user: signed }The 5-minute window
A signature is only valid for 300 seconds after its _ts. This keeps a captured context from being replayed forever. For a long-lived session, re-sign periodically (a fresh _ts and _sig) rather than reusing one signature all day. A short-lived endpoint that signs on each page load is usually enough.
IDENTITY_SIGNATURE_EXPIRED. A missing or wrong signature returns IDENTITY_SIGNATURE_REQUIRED or IDENTITY_SIGNATURE_INVALID. Check the clock skew on your server first, then confirm you are stringifying keys in sorted order.