Dokümantasyon
Oturum açma ve kimlik
Bu sayfada
Your customer is already signed in to your product. Busymate AI can know who they are, and your own tools can act as them. No second login. No "Authorize" card.
Contract
bmai-identity2.0.0 ·@busymate/identity-contract2.0.0-beta.2. This page is generated from the contract. Anything marked [PLANNED] is not live yet. Machine-readable: busymate.ai/.well-known/bmai-integration.json.
| Part | Version |
|---|---|
| Launch token | v1 (unchanged) |
| Actor token | v3 (ES256, public JWKS) per workspace. v2 (HS256, your secret) stays until it is removed |
| Web SDK | @busymate/ai-web 0.1.1 on npm |
| Verifier | @busymate/ai-verify 0.1.2 on npm |
| Check CLI | @busymate/identity-check 0.1.2 on npm |
| Apps | SDK 1.0.2 (iOS, Android, macOS, Windows, Linux, Electron, Tauri), bridge busymate.bridge/2 2.0.0 |
| Per-connector customer identity | busymate | oauth | none [PLANNED] |
How it works
Your site vouches for the user with a short-lived launch token. Busymate AI verifies it and opens an identified session. When a question needs their account, Busymate AI calls your MCP server or API with an actor token, and you act as that user. No shared account database. No passwords with Busymate AI.
Choose your setup
| You have | Do | Steps |
|---|---|---|
| A website with the widget | Your page answers getIdentity | Setup A |
A full chat page (https://acme.busymate.ai or your domain) | Your login page hands the user back | Setup B |
| An iOS, Android or desktop app | The app bridge | Setup C |
| Your own MCP server or API | Verify the actor token | Setup D |
Every setup starts with Step 1. Setup D removes the "Authorize" card.
| Guest | Identified | |
|---|---|---|
| Who | Anyone | Signed in to your product |
| History | This browser | Follows the customer |
| Public tools | Yes | Yes |
| Your account tools | No | Yes, as that customer |
Step 1: Sign your user
- Create an ES256 key pair. Publish the public key as a JWKS at
https://<issuer>/.well-known/jwks.json. - Add one endpoint, body
{ nonce }. It needs your session. Signed out:401. Signed in:201 { token, nonce, expiresIn }withCache-Control: no-store. - Sign
iss=https://api.acme.com,aud=busymate-ai,sub= your immutable id for one person (never an e-mail),tenant_id=acme, the requestnonceunchanged, a uniquejti,iat, andexp= now + 120 s.
import { SignJWT, importPKCS8 } from "jose";
import { randomUUID } from "node:crypto";
const key = await importPKCS8(process.env.BMAI_ES256_PRIVATE_KEY, "ES256");
app.post("/api/bmai/identity", requireYourSession, async (req, res) => {
const { nonce } = req.body ?? {};
if (typeof nonce !== "string" || nonce.length < 16) return res.status(400).end();
const token = await new SignJWT({ tenant_id: "acme", nonce })
.setProtectedHeader({ alg: "ES256", kid: process.env.BMAI_KID })
.setIssuer("https://api.acme.com").setAudience("busymate-ai")
.setSubject(String(req.user.id))
.setJti(randomUUID()).setIssuedAt().setExpirationTime("120s")
.sign(key);
res.set("Cache-Control", "no-store").status(201).json({ token, nonce, expiresIn: 120 });
});- Register it: Console › Identity (or
upsert_tenant_identity_provider). Enter issuer, JWKS URL, audience, tenant claim, mint URL andlogin_url. Runtest_tenant_identity_provider, then publish. An unpublished provider is refused. Identity edits apply without a republish. [PLANNED]
PHP, Python and Go samples: https://busymate.ai/sdk/v1/kit/backend/.
Setup A: the widget on your site
Add your page origin under Console › Integration › Allowed origins. Three calls: getIdentity answers the chat, identityChanged() after sign-in or an account switch, signedOut() after logout.
<script>
window.BusymateAI = {
getIdentity: async ({ nonce }) => {
const r = await fetch("https://api.acme.com/api/bmai/identity", {
method: "POST", credentials: "include", cache: "no-store",
headers: { "content-type": "application/json" },
body: JSON.stringify({ nonce }),
});
if (r.status === 401 || r.status === 403) return null; // signed out
if (!r.ok) throw new Error("identity mint failed " + r.status);
const id = await r.json();
if (id.nonce !== nonce) throw new Error("nonce mismatch");
return { token: id.token, nonce: id.nonce }; // never cache
},
};
// after YOUR login or account switch: window.BusymateAI.identityChanged();
// after YOUR logout: window.BusymateAI.signedOut("signed_out");
</script>
<script src="https://acme.busymate.ai/embed/v1.js" data-assistant="acme" async></script>With a bundler: npm install @busymate/ai-web (0.1.1). createBusymateChat({ assistant: "acme", origin: "https://acme.busymate.ai", getIdentity: mintFromEndpoint("https://api.acme.com/api/bmai/identity") }), then chat.identityChanged(), chat.switchAccount() and chat.logout() on your auth events.
Signed out? The widget shows Sign in:
| Your page | What Sign in does |
|---|---|
Registered a sign_in page tool | A card opens in the thread: email and code, password or one button. Nothing leaves your page. |
No tool, login_url set | Your login page opens in a popup or the top window, signs the user in, and returns to return_to#bmai_token=…&bmai_nonce=…. The widget picks it up. |
| Neither | Nothing is offered. |
Setup B: the full chat page
There is no code on https://acme.busymate.ai, so your login page does the work. Register it as login_url.
| Busymate AI sends | Your login page |
|---|---|
?return_to=<chat>&bmai_nonce=<n> (user tapped Sign in) | Signs the user in (password, email code, SSO), mints with <n>, redirects to return_to#bmai_token=<token>&bmai_nonce=<n> |
the same plus bmai_prompt=none (silent, once per tab) | Never shows UI. Session: redirect as above. None: return_to#error=login_required |
A user already signed in is recognised with no click when your login page can load in a hidden frame on the chat host:
Content-Security-Policy: frame-ancestors https://assistant.acme.com https://acme.busymate.ai
Set-Cookie: <your session>; Secure; SameSite=NoneWithout both, signed-in users stay guests until they tap Sign in. The silent check gives up after 3 seconds. You can also link straight in: https://acme.busymate.ai/#bmai_token=<t>&bmai_nonce=<n>.
Setup C: your own app
Pass account and mint. Call accountChanged() on login, logout and account switch. Install the bridge before the first load.
BusymateBridge.install(on: webView, config: .init(
assistant: "acme", origins: ["https://acme.busymate.ai"],
account: { Auth.shared.userId }, // nil when signed out
mint: { request in try await API.mintBmaiIdentity(nonce: request.nonce) } // never cached
))
webView.load(URLRequest(url: URL(string: "https://acme.busymate.ai/?channel=ios")!))BusymateBridge.install(webView, BusymateBridge.Config(
assistant = "acme", origins = listOf("https://acme.busymate.ai"),
account = { auth.userId },
mint = { request, done -> api.mintBmaiIdentity(request["nonce"] as String) { done(it) } }
))
webView.loadUrl("https://acme.busymate.ai/?channel=android")SDK 1.0.2: iOS, Android, macOS, Windows, Linux, Electron, Tauri. Desktop uses channel=desktop_sdk, never desktop. React Native and Flutter use the frozen files at https://busymate.ai/sdk/v2/2.0.0/. Copy each channel's exact chat URL from Console › Integration.
Setup D: your MCP server or API
Busymate AI sends an actor token on each tool call by an identified customer. Verify it before your own auth middleware.
npm install @busymate/ai-verify # Node 18+ and Deno. Python, PHP, Go: same repositoryimport { verifyBusymateActor, requireConfirmed, BusymateActorError } from "@busymate/ai-verify";
try {
const actor = await verifyBusymateActor(req, {
audience: "https://mcp.acme.com", // EXACT origin of your connector URL
tenantId: "acme",
connectorId: process.env.BMAI_CONNECTOR_ID!, // Console › Connections
idp: process.env.BMAI_IDP, // Console › Identity
tool: toolName, // the tools/call name
});
const user = await users.findByBusymateIdentity(actor.idp, actor.sub); // (idp, sub) only
requireConfirmed(actor, WRITE_TOOLS.has(toolName)); // money and writes
// run the tool as `user`. Never read an account id from tool arguments.
} catch (e) {
if (e instanceof BusymateActorError) return reply(401, { "WWW-Authenticate": 'Bearer error="invalid_token"' });
throw e;
}Serve GET /api/bmai/status returning {"actorVerifier": true, "actor": 3}. Then Busymate AI sends token v3 in Authorization; until then in X-Bmai-Actor.
Rules: map users by (idp, sub) only. Require confirmed for money and writes. Reject a repeated jti. Answer a bad token with 401, never fall through to another user. Guests get no token on a v3 workspace.
Who sees a card
Set per connector in the Console. The three modes below are the replacement for today's Signed actor token and Per-customer OAuth settings. [PLANNED]
| Customer identity | For | The signed-in customer sees |
|---|---|---|
| Busymate AI token [PLANNED] | Your own product | No OAuth card. With "Ask customers" ON: one confirm. If your MCP rejects the token: "temporarily unavailable", and you get an alert. |
| OAuth [PLANNED] | A third party with its own OAuth | "Authorize {partner}" once |
| None [PLANNED] | Public tools only | No card |
Ask customers before using their account is one switch, default OFF. Busymate AI enables the token mode only after a live actor probe passes: it mints a test-account token for your connector origin and your server must accept it. On the same registrable domain as your sign-in it is the default, after the probe. [PLANNED] Today a per-customer OAuth connector shows the card once per customer, even when they are signed in.
Sign out and identity change
Call signedOut(), logout() or accountChanged() on every logout, or the next person sees the previous chat. On an account switch call identityChanged() (SDK switchAccount()): the same conversation upgrades in place. A signed-out customer loses account tools at once. The nonce is always the one Busymate AI gives you: never your own, never cached.
Channels
| Channel | Today |
|---|---|
| Widget, full chat page, iOS, Android, desktop | Identified, as above |
| WhatsApp, Messenger, Telegram | Identified through your login_url when you switch it on (below). Off: guests only |
| SMS, Instagram, Apple Messages, Slack, Teams, Discord, e-mail | Guests only. No account tools |
| ChatGPT and Claude apps | Your team only, not your customers |
Channel sign-in. Set channels.customerSignIn to the channels you want (["whatsapp", "messenger", "telegram"]) and publish. It needs a published identity provider with a login_url. The customer sends sign in (or /login) in the chat and opens a one-time link on your chat domain. Your login_url gets return_to, bmai_nonce and bmai_client=channel, and answers exactly as for the web. The page then shows a 6-digit code. The customer sends that code in the same chat, and only then is the chat linked to your customer (sub). After that, account tools run as that customer with no card. A link forwarded to someone else cannot link their account to the sender's chat, because the code only works in the chat that asked. sign out unlinks the chat. A link lasts 30 days, and you can end it sooner by disabling or unpublishing the provider.
Verify
-
GET https://api.acme.com/.well-known/jwks.jsonreturns your key. Mint:401signed out,201+no-storesigned in, nonce echoed. - Widget: sign in and the chat greets you in the same conversation. Sign out and the transcript clears.
- Full chat page: open it already signed in. Recognised in about 3 s, no click.
- Ask for something only your account tools answer: no card.
list_identity_link_eventsshows identified, token minted, connector accepted. - Your server rejects a wrong
aud, an expired token and an anonymous token.
Busymate AI probes you three ways: the live actor probe, your GET /api/bmai/status, and the Console readiness page. Run the same checks yourself:
npx @busymate/identity-check --issuer https://api.acme.com --mint https://api.acme.com/api/bmai/identity \
--login-url https://acme.com/login --chat-origin https://acme.busymate.ai \
--mcp https://mcp.acme.com --probe-url https://mcp.acme.com/mcp --tenant-id acmeExit 0 passed, 1 a check failed, --json for machines. Browser version: Identity check.
Troubleshooting
| You see | Cause | Fix |
|---|---|---|
Signed in to your product, still asked to Authorize (reason third_party) | The connector is per-customer OAuth, or not treated as yours, or your MCP does not accept the actor token | Do Setup D. Make sure your MCP verifies the actor token before its auth middleware, and the connector uses Signed actor token |
The Authorize card keeps coming back (partner_rejected) | Your server answered 401 to the actor token | Check aud (exact origin), tenant_id, connector_id, and that the verifier runs first |
Always a guest (provider_not_published) | The provider is saved but not published | Publish the workspace |
nonce_mismatch | You made your own nonce, or cached a token | Use the nonce Busymate AI passes you. Mint a new token on every call |
409 on launch | A jti or nonce was reused | A unique jti per token. Never cache |
claim_invalid, token_expired, clock_skew | Claims differ from the registration, the token is older than the maximum token age, or the server clock is off | Match the registration. Use 120 s. Sync the clock |
| Full chat page: signed-in users stay guests | The login page cannot be framed, or its cookie is not SameSite=None | Send frame-ancestors <chat origins> and SameSite=None; Secure |
| The silent check shows a visible page | Your login page ignored bmai_prompt=none | Never render UI on prompt=none: redirect with #error=login_required |
Account tools are hidden (asks_identity_not_conveyed) | The connector identity mode is none | Set Signed actor token |
| "Sign in before this account tool can run" | The visitor is not identified | Fix Step 1 and your setup first |
Your MCP answers 401 with confirmation_required | A write tool was called without confirmed | Nothing to fix: the customer must confirm in the chat |
| The previous user's chat is visible after logout | signedOut() was not called | Call it on every logout |
credential_missing on WhatsApp, Messenger or Telegram | The chat customer is not signed in | Switch on channels.customerSignIn and ask the customer to send sign in |
The reason in a 401 launch body and the chat's rejected:<code> come from one list of codes: see the manifest.
Example: RentADriver
Chat: assistant.rentadriver.ai. MCP: https://mcp.rentadriver.ai/mcp. Issuer and mint: https://api.rentadriver.ai. Three changes remove the card:
- One person per
subin the launch token, not an organization id. - The verifier before the auth middleware on
mcp.rentadriver.ai:verifyBusymateActor(req, { audience: "https://mcp.rentadriver.ai", tenantId, connectorId, idp, tool }). The ids are given at cut-over. confirmedfor write tools:create_delivery,fund_delivery,tip_driver,create_wallet_deposit,cancel_delivery,set_spending_controls, key and webhook tools.
Then Busymate AI sets the connector to the token mode. [PLANNED] Until then the Authorize card is the working fallback.
Reference
| Item | Value |
|---|---|
| Launch token | ES256 or RS256. exp - iat within the provider's maximum (30 to 900 s, use 120). Skew up to 120 s. One-time jti and nonce: a replay gets 409. |
| Actor token v3 | ES256, typ: bmai-actor+jwt, keys at https://busymate.ai/.well-known/bmai-actor-jwks.json (rotate every 90 days, 14-day overlap: never pin a key). iss = https://busymate.ai. Claims: aud, sub, idp, tenant_id, connector_id, support_session_id, actor_kind: "identified", confirmed, tool, iat, nbf, exp (at most 120 s after iat), jti. Skew 10 s. |
| Actor token v2 (compat) | HS256, kid: bmai-support-v2, Authorization: Bearer, at most 300 s, also sent for anonymous visitors (sub = anonymous:<session>): check actor_kind. Set a secret of 32+ bytes in Console › Connections › Actor verifier. The connector must be Signed actor token, server auth not OAuth, tools Identified. Removed after a 90-day overlap. |
| Reason codes | The full list is in the manifest. |
Questions
For AI coding agents
Copy this block into your agent with the values above filled in.
GOAL: The Busymate AI chat must recognise the signed-in user and call our MCP/API as that user, with no extra login and no Authorize card.
CONTRACT: bmai-identity 2.0.0 (@busymate/identity-contract 2.0.0-beta.2). Manifest: https://busymate.ai/.well-known/bmai-integration.json
1. BACKEND (language of this repo)
- ES256 key pair. Serve the public key as a JWKS at <issuer>/.well-known/jwks.json (kid, alg ES256, use sig).
- POST <mint path>, body {nonce, returnTo?}, needs OUR existing session. Signed out: 401. Else 201 {token, nonce, expiresIn:120}, header Cache-Control: no-store.
- JWT claims: iss=<issuer>, aud=<audience>, sub=<immutable per-person user id>, <tenant claim>=<tenant>, nonce=<request nonce unchanged>, jti=<uuid>, iat=now, exp=now+120.
- Never an e-mail or phone in sub. Never a shared organization id. Never cache or store a token. Never make your own nonce.
2. FRONTEND (pick what this repo is)
- Plain site: define window.BusymateAI.getIdentity({nonce}) BEFORE loading <chat origin>/embed/v1.js (data-assistant=<slug>). Call window.BusymateAI.identityChanged() after login, rotation and account switch, and window.BusymateAI.signedOut("signed_out") after logout.
- Bundler: npm install @busymate/ai-web. createBusymateChat({assistant, origin, getIdentity: mintFromEndpoint(<mint url>)}); chat.mount(); identityChanged() / switchAccount() / logout() on our auth events.
- iOS, Android, desktop: BusymateBridge.install(... account, mint ...) BEFORE the first load; accountChanged() on every auth change. Desktop uses channel=desktop_sdk, never channel=desktop. React Native and Flutter use the frozen bridge files.
- Hosted chat page or custom domain: our login page reads return_to, bmai_nonce and bmai_prompt, mints with that nonce and redirects to return_to#bmai_token=<token>&bmai_nonce=<nonce>. On bmai_prompt=none it never renders UI: no session means redirect to return_to#error=login_required. Send Content-Security-Policy frame-ancestors <chat origins> and a SameSite=None; Secure session cookie.
3. MCP SERVER / API
- npm install @busymate/ai-verify (Python, PHP, Go files: github.com/serebano/busymate-ai-sdk-verify).
- Before our own auth middleware call verifyBusymateActor(req, {audience: <exact connector origin>, tenantId, connectorId: BMAI_CONNECTOR_ID, idp: BMAI_IDP, tool: <tools/call name>}).
- Token v3: ES256, typ bmai-actor+jwt, header X-Bmai-Actor (or Authorization: Bearer when GET /api/bmai/status returns {"actorVerifier":true,"actor":3}); keys at https://busymate.ai/.well-known/bmai-actor-jwks.json; iss https://busymate.ai; exp-iat <= 120; clock skew <= 10 s.
- Map the user by (idp, sub) ONLY. Never from a tool argument. Require confirmed === true (requireConfirmed) for money and write tools. Reject a repeated jti.
- On a BusymateActorError answer 401 with WWW-Authenticate: Bearer error="invalid_token"; log e.reason; never fall through to another user.
- Serve GET /api/bmai/status -> {"actorVerifier": true, "actor": 3}.
- Compat v2 (HS256, kid bmai-support-v2, header Authorization: Bearer, exp-iat <= 300, secret set in the Console): check actor_kind === "identified"; anonymous means public data only.
4. DO NOT: make your own nonce; cache or store tokens; use channel=desktop; use OAuth mode for our own MCP; put an account id in tool arguments; ask the human for private keys.
5. TELL THE HUMAN to: register the provider and publish (Console > Identity), set the connector to Signed actor token (or ask for token v3 on the workspace), keep "Ask customers before using their account" OFF unless they want the confirm, then run: npx @busymate/identity-check --issuer <issuer> --mint <mint url> --login-url <login url> --chat-origin <chat origin> --mcp <connector origin> --tenant-id <tenant>.Changelog
2.0.0-beta.2 · 2026-10-29
- One public identity page, generated from this contract.
- Actor token v3 (ES256, public JWKS) is live behind a per-workspace flag, dual-signed beside v2. Guests get no token on a flagged workspace.
@busymate/ai-verifyand@busymate/identity-checkare on npm.@busymate/ai-webis on npm.
2.0.0-beta.1 · 2026-10-29
- The identity contract package, the machine manifest
/.well-known/bmai-integration.jsonand signed test vectors. - Launch verifier constants (algorithms, token age, required claims) now come from the contract. No behaviour change.
Sorular
Do my customers need an account with Busymate AI?
No. They sign in to your product. Your backend signs the launch token, and the subject is your own user id.
Why does a signed-in customer still see "Authorize"?
Your connector is per-customer OAuth, or your MCP does not accept the actor token yet. Do Setup D, then check Troubleshooting.
Can the assistant read a password?
No. Credentials go to your own login page or sign-in card.