SMeetMini Apps

SMeet Mini Apps

Sign-in

The person is already signed in to SMeet. The page asks SMeet for a one-time login code, its server exchanges the code for a Mini App session, and from then on the page talks to its server with a session of its own. No cookie takes part.

How it works#

  1. The page asks its own server for a login attempt, {attempt_id, attempt_secret}: two random values, used once, valid for about two minutes. The page keeps them in memory.
  2. The page calls app.authorize(). The SDK makes a PKCE verifier of 32 random bytes and sends SMeet only its S256 challenge.
  3. SMeet checks that the window may still be used and answers with a login code, smlc_..., valid for 60 seconds. The SDK returns {code, codeVerifier, expiresAt}.
  4. The page sends {attempt_id, attempt_secret, code, code_verifier} to its server.
  5. The server uses up the attempt and calls exchangeLaunchCode with its key and an Idempotency-Key bound to the attempt. SMeet checks the verifier against the challenge and answers with a Mini App session, smas1_..., and who signed in.
  6. The server keeps the SMeet session and gives the page a session of its own in the response body. The page keeps it in memory and sends it as Authorization: Bearer with every request to the server.

The SMeet session (smas1_...) and the key of the server (smac1_...) never reach the page. The code and the verifier travel only between the page and its server.

The login attempt#

The attempt is the state of one sign-in, kept by your server instead of a cookie. It binds the code the page brings back to the page that started the sign-in: a code that a foreign page got from SMeet cannot be exchanged into a session of this page without this page's secret.

  • Make both values random, at least 128 bits each, and keep only a hash of the secret.
  • Use an attempt once: take it out atomically before the exchange, whatever the outcome, and compare the secret in constant time.
  • Let it expire after about two minutes.

The minimal Python example keeps attempts in memory:

Pythondocs/mini-apps/examples/python-minimal/server.py
def new_attempt(self) -> Dict[str, Any]:
    attempt_id, attempt_secret = secrets.token_urlsafe(18), secrets.token_urlsafe(32)
    with self.lock:
        now = self.clock()
        self.attempts = {k: v for k, v in self.attempts.items() if v[1] > now}
        self.attempts[attempt_id] = (hashlib.sha256(attempt_secret.encode()).digest(), now + ATTEMPT_TTL)
    return {"attempt_id": attempt_id, "attempt_secret": attempt_secret}
Pythondocs/mini-apps/examples/python-minimal/server.py
def consume_attempt(self, attempt_id: str, attempt_secret: str) -> bool:
    with self.lock:
        item = self.attempts.pop(attempt_id, None)
    if item is None:
        return False
    same = hmac.compare_digest(item[0], hashlib.sha256(attempt_secret.encode()).digest())
    return same and item[1] > self.clock()

A server with more than one process keeps attempts in its database, for example with UPDATE ... SET used_at = now() WHERE id = $1 AND used_at IS NULL AND expires_at > now().

authorize()#

JavaScript
const attempt = await api("login/attempt");             // your server
const { code, codeVerifier, expiresAt } = await app.authorize();
const session = await api("login/exchange", {
  attempt_id: attempt.attempt_id, attempt_secret: attempt.attempt_secret, code, code_verifier: codeVerifier,
});
  • Ask for the attempt first, then call authorize(): the code lives 60 seconds, and an attempt fetched afterwards eats into them.
  • SMeet answers the code only over the channel of the page that asked, the document SMeet loaded and checked. A request of an earlier or another document of the window never gets it. A page that navigates to another page of the same app signs in again there.
  • The verifier lives only in the memory of the page; the SDK does not store it.
  • Call authorize() in your page's start code, for example in window.onload after connect() and ready(). SMeet asks the person nothing at this point: the consent was given before the window opened.

The exchange#

Your server takes up the attempt, then exchanges the code. The key of the request is bound to the attempt, so a repeat after a lost answer gets the same session and nothing else does:

Pythondocs/mini-apps/examples/python-minimal/server.py
def sign_in(self, body: Dict[str, Any]) -> Dict[str, Any]:
    attempt_id, attempt_secret = body.get("attempt_id"), body.get("attempt_secret")
    code, verifier = body.get("code"), body.get("code_verifier")
    if not (isinstance(attempt_id, str) and ATTEMPT_RE.match(attempt_id) and isinstance(attempt_secret, str)
            and isinstance(code, str) and 0 < len(code) <= 512 and isinstance(verifier, str) and VERIFIER_RE.match(verifier)):
        raise PageError(400, "INVALID_REQUEST")
    if not self.consume_attempt(attempt_id, attempt_secret):
        raise PageError(400, "LOGIN_RETRY")
    # The key is bound to the attempt: a repeat of this exchange gets the same session (6.4), nothing else does.
    key = f"login:{attempt_id}"
    for round_ in range(3):
        try:
            exchanged = self.author("exchangeLaunchCode", {"code": code, "code_verifier": verifier}, key)
            break
        except SMeetError as error:
            if error.code in ("NETWORK", "TEMPORARILY_UNAVAILABLE", "RATE_LIMITED") and round_ < 2:
                time.sleep(min(5, error.retry_after or 1))
                continue
            raise page_error(error)
    if exchanged.get("environment") != self.settings.environment:
        raise PageError(500, "SERVER_MISCONFIGURED")
    token = secrets.token_urlsafe(32)
    with self.lock:
        self.sessions[token] = {"smeet": exchanged["session_token"], "user": exchanged.get("user") or {},
                                "expires": min(self.clock() + SESSION_TTL, parse_time(exchanged.get("expires_at")))}
    user = exchanged.get("user") or {}
    return {"session_token": token, "environment": exchanged.get("environment"),
            "expires_at": exchanged.get("expires_at"),
            "user": {"id": user.get("id"), "display_name": user.get("display_name")}}
  • Compare environment of the answer with the environment of your server; a key of the test version never exchanges a production code (ENVIRONMENT_MISMATCH), and the check guards against a key put into the wrong server.
  • user.id is the person's id in the Bot API, the same as from.id in your bot's updates. chat.id is the private chat of the person with your bot.
  • start_param is the parameter of the button or link that opened the window, or null.

The answer and every field are described in the API reference.

Your app's own session#

Do not hand the SMeet session to the page. Give the page a session of your server, in the body of the answer, and keep it in the page's memory:

  • send it as Authorization: Bearer <token>, never as a cookie and never in an address;
  • let it end no later than expires_at of the SMeet session (15 minutes at most);
  • before every protected operation, check the SMeet session behind it with checkSession.

localStorage and sessionStorage of the page may hold a draft, but not a session: they outlive the window and are shared by everything on your origin.

Renewal#

A Mini App session lives 15 minutes at most. While the window is open, the page renews its session about two minutes before it ends: a new attempt, a new authorize(), a new exchange. There is no refresh token.

JavaScript
function scheduleRenewal(session) {
  const wait = Math.max(10_000, Date.parse(session.expires_at) - Date.now() - 120_000);
  setTimeout(() => signIn().catch(showSignInError), wait);
}

When the launch is closed (the person closed the window on another device, revoked access, signed out), SMeet closes the window itself; authorize() then fails with LAUNCH_CLOSED.

Errors#

authorize() fails with a bridge code:

CodeWhat to do
CODE_EXPIREDCall authorize() again, with a new attempt
NETWORKCall authorize() again
INTERNALA fault of SMeet; call authorize() again, at most a few times
RATE_LIMITEDWait: 60 codes a minute per person
LAUNCH_CLOSEDStop: SMeet tells the person why and closes the window
MAINTENANCEMini Apps are paused; say so and keep the person's draft
NOT_READYThe window shows an error screen; nothing to do in the page

The exchange on your server fails with a code of the author API:

CodeWhat to do
CODE_NOT_FOUND, CODE_EXPIRED, CODE_REVOKED, CODE_ALREADY_USED, CODE_VERIFIER_MISMATCHThe page signs in again: a new attempt and a new authorize()
ACCESS_DENIEDThe person may not use the app now; reason says why (Reasons)
TEMPORARILY_UNAVAILABLE, RATE_LIMITED, a network error, a 502, 503 or 504Repeat the same request with the same key after retry_after, within 60 seconds of the exchange
MINI_APPS_MAINTENANCEMini Apps are paused; the page says so
INVALID_CREDENTIALS, APP_MISMATCH, ENVIRONMENT_MISMATCHThe server's key is wrong or belongs to another app or environment: fix the configuration

Why there are no cookies#

Inside the SMeet window your page is a frame of another site. Browsers treat cookies there as third-party cookies, and Safari blocks them. A sign-in that depends on a cookie works in one browser and fails in another. The login attempt replaces the cookie as the state of the sign-in, and the session in the page's memory replaces the session cookie.