Mihe

User tokens

Tell the assistant who the user is, and what they may do, from your own backend.

The SDK runs in a browser the user controls, so nothing it says about itself can be believed. Instead, your backend signs a short-lived token for the signed-in user, and the runtime reads their identity and permissions from its verified claims, and from nothing else.

The token

A JWT, signed with HS256 using the app's secret key: the whole sk_… string, exactly as the dashboard showed it.

ClaimRequiredMeaning
subyesYour id for the user. It appears in the audit log and the usage feed.
expyesWhen it expires, in Unix seconds.
permissionsnoThe user's permission strings, matched against each action's permissions. Omitted means none.
rolenoAccepted, but grants nothing. Permissions alone decide what the user may do.
iatnoWhen it was issued. If present, the lifetime is measured from it.

A token may live one hour at most. Longer is refused, so a leaked token stops working before it is useful. Fifteen minutes is a good default. The SDK asks for a fresh one each time it connects, and again when the runtime says the current one has expired, so a short lifetime costs nothing.

Signing it

Mint it from your own session, with the user's real permissions, on every request. Never cache one token for everybody.

Node
import jwt from "jsonwebtoken";

const token = jwt.sign(
  { sub: user.id, permissions: user.permissions },
  process.env.MIHE_SECRET_KEY!,
  { algorithm: "HS256", expiresIn: "15m" },
);
Python
import os, time, jwt  # PyJWT

token = jwt.encode(
    {"sub": user.id, "permissions": user.permissions, "exp": int(time.time()) + 900},
    os.environ["MIHE_SECRET_KEY"],
    algorithm="HS256",
)

Serve it from a route the SDK can call, and hand that route to getUserToken:

<AssistantProvider
  getUserToken={() => fetch("/api/mihe-token").then((r) => r.text())}
  // …
/>

The route must only answer for a signed-in user. It is how the assistant knows who is asking.

Keep the secret key on the server

Anyone holding the secret key can sign a token for any user, with any permissions. Keep it in your backend's environment. Never put it in a browser bundle or a NEXT_PUBLIC_-style variable. A session opened from a browser with it is refused.

When a token is refused

The session is refused with one of these codes, and its message appears as lastError on useAssistant() and in the widget:

CodeCause
invalid_user_tokenNot signed with this app's secret key. Check you are using the key for this app.
expired_user_tokenExpired. Mid-session the SDK fetches a new one by itself; if it happens on connecting, check your server's clock and that your route signs a fresh token each time.
user_token_lifetime_too_longValid for more than an hour. Sign shorter-lived tokens.
user_token_missing_expiryNo exp claim.
user_token_missing_subjectNo sub claim.
malformed_user_tokenNot a JWT, or not HS256.

On this page