KUMODeck
日本語

Authentication

Your users (called players in the API) start using your app or game before they sign up. Kumo.init() creates a guest on first visit and resumes it on every visit after that; when a user cares about their data, they attach an email, Google, Discord, Apple or X to the same account and keep everything.

Guests (automatic)#

const kumo = await Kumo.init({ projectKey: 'pk_live_…' });
kumo.auth.player;        // { id, displayName, isGuest: true, createdAt, identities: [{ provider: 'guest' }] }
await kumo.auth.setDisplayName('Ace');   // shown in rooms

Pass autoGuest: false to init if you want to show your own sign-in screen first. Sessions are kept in localStorage per publishable key (so dev and prod never mix); if storage is blocked (private mode, some iframes), the SDK falls back to memory and the user is a new guest on each reload.

Email and password#

// turn the current guest into a permanent account — their data stays
await kumo.auth.linkEmail('[email protected]', 'correct horse battery');

// on another device
await kumo.auth.signInWithEmail('[email protected]', 'correct horse battery');

// a brand-new account without a guest phase
await kumo.auth.signUpWithEmail('[email protected]', 'correct horse battery', 'Ace');

await kumo.auth.signOut();
kumo.auth.onChange((player) => renderAccount(player));   // null when signed out

Passwords need at least 8 characters. Sign-in errors never reveal whether an email is registered.

The KUMODeck news box on your sign-up screen (on by default)#

Email sign-up screens show one more box, drawn by KUMODeck: "Get news from KUMODeck" — KUMODeck's own news (new games and features, up to 2 emails a month). It is on by default: put it on your screen, even if you build the screen yourself. You can remove it for your app (below).

const news = await kumo.news.mountNewsOptIn(document.querySelector('#news-box'));  // draws the box (or nothing)
await kumo.auth.signUpWithEmail(email, password);
await news.submit(email);   // does nothing unless the user ticked it

Why this is fine for you and your users:

  • The user chooses. It is a separate box, unchecked at first, and signing up works the same without it.
  • A confirmation email decides. Nothing is sent until the user clicks the link in KUMODeck's confirmation email.
  • It costs you nothing. KUMODeck pays for this list; it is not on your bill.
  • Only adults see it; apps and games for kids (audience: "kids") never show it. The box names KUMODeck and says it is the service that runs your app — KUMODeck draws the words so they are always right, so do not write your own.

To remove the box from your app, turn it off in the dashboard (Overview → "KUMODeck news box") or in kumo.config.json and push:

kumodeck features off kumoNews && kumodeck config push --env production   # writes "kumoNews": { "optIn": false }

Keep the code as it is: with the box removed, mountNewsOptIn draws nothing and submit does nothing. Users who already signed up keep getting KUMODeck's news until they stop it. Turn it back on with kumodeck features on kumoNews.

Sessions#

TokenLifetimeStored
Access token (JWT)15 minutesmemory + localStorage
Refresh token90 days, rotated on every uselocalStorage

You never handle tokens: the SDK refreshes on token_expired and retries once. Tabs of the same app coordinate through the Web Locks API so two tabs never race the same refresh token. Replaying an already-used refresh token revokes the whole session chain (a stolen token dies the moment either party uses it again).

Signing out ends the refresh token, but an access token already handed out keeps working until it expires, up to 15 minutes (it is checked by its signature, without a lookup). The SDK forgets it on sign-out, so this only matters if the token was copied elsewhere. A ban stops the user's writes at once; their reads keep working until the next refresh (at most 15 minutes), which then fails and signs them out.

Sign in with Google, Discord, Apple or X#

Every provider is off until you turn it on in kumo.config.json:

kumo.config.json
{
  "auth": {
    "redirectUrls": ["https://mygame.example.com/*"],
    "providers": {
      "google": { "enabled": true },
      "x": { "enabled": true }
    }
  }
}
ProviderWhat you set up
XRegister your own X app and enter its OAuth 2.0 Client ID and Client Secret in the dashboard under Sign-in methods → X. Steps: Your own X app
Google, Discord, AppleCreate an OAuth client with the provider and enter its client id and secret (Apple: Services ID, Team ID, Key ID and private key) in the dashboard under Sign-in methods. The redirect URI to register is shown there (…/v1/auth/oauth/<provider>/callback). Credentials are stored encrypted and never go in the config file
// popup by default — call it from a click handler
await kumo.auth.signInWithProvider('google');
await kumo.auth.signInWithProvider('discord', { mode: 'redirect' });   // for in-app browsers
await kumo.auth.linkProvider('apple');                                 // attach to the current guest, keep progress

// after a redirect, the result is picked up by Kumo.init(); read it with:
const result = await kumo.auth.completeRedirectSignIn();   // { player, provider, linked } or null
  • Return URLs are allow-listed in auth.redirectUrls (exact URLs, or a trailing * for a path prefix). Localhost in development and your KUMODeck-hosted URLs are allowed automatically. This stops attackers from receiving sign-in codes on their own site.
  • OAuth uses PKCE: the code that comes back in the URL fragment is useless without a verifier that exists only in the user's browser.
  • Accounts are never merged by email. An identity that already belongs to another user fails with identity_already_linked.
  • Inside X's in-app browser the default mode is redirect, because popups are unreliable there.
  • kumo.auth.unlinkIdentity(provider) removes a sign-in method (for example one the user doesn't recognise) and signs out every other device. The last way to sign in can't be removed: if no other provider or email-with-password would be left, it fails with 409 last_login_method and nothing changes. Otherwise the user could lose the account (and their saves) the moment this device is signed out. Ask the user to add another method first (linkEmail / linkProvider), then remove the old one:

``js try { await kumo.auth.unlinkIdentity('google'); } catch (e) { if (e instanceof KumoError && e.code === 'last_login_method') showHint('Add another sign-in method first'); else throw e; } ``

Your own X app#

Sign in with X runs on an X app you register, like the other providers. Until you save one, starting Sign in with X fails with 409 provider_not_configured. It takes 10–20 minutes, once per app (or studio):

  1. Sign in to console.x.com with your X account, accept the Developer Agreement and Policy, and describe your use (for example: "users of my web app sign in with X; we only read their public profile").
  2. Create an app in the pay-per-use Production environment. Apps in the old Free / development environment are refused by X (403 client-not-enrolled).
  3. Name, description and icon: your app's name is fine — it is what X's consent screen shows. Don't use "X", "Twitter" or the X logo in any of them.
  4. Under User authentication settings, turn on OAuth 2.0 with app type Web App (confidential client). KUMODeck adds PKCE for you, so there is nothing to set for it.
  5. Scopes: only users.read and tweet.read.
  6. Callback URL: the …/v1/auth/oauth/x/callback URL shown in the dashboard under Sign-in methods → X, exactly as shown.
  7. Fill in your website, terms of service and privacy policy URLs. Your privacy policy should say what you get from X (handle, name, avatar, X user id).
  8. Generate the OAuth 2.0 Client ID and Client Secret (X shows the secret once), paste them in the dashboard under Sign-in methods → X and save, then turn on auth.providers.x.enabled.

X bills your X app directly for its API use; KUMODeck does not charge for it. According to X, reading the signed-in user's profile (/2/users/me) for sign-in is free.

X's sign-in rules apply to your app's screens: make the Sign in with X button at least as prominent as your other sign-in options, show the user their X handle, avatar and the X logo after sign-in (kumo.x.profile()), and link to your privacy policy before sign-in.

Where KUMODeck's shared X app is available, the dashboard also offers Use without setup instead of your own app. See Sharing on X.

Email verification and password reset#

Email links come back to your app's page (it must be in auth.redirectUrls). If you don't pass a redirectUrl, the link opens a small, unbranded built-in page on your app's domain (/.auth/verify, /.auth/reset) and finishes the job there. Passing your app's URL lets your own screen handle it.

await kumo.auth.resendVerification();                     // for the signed-in email account
await kumo.auth.sendPasswordReset('[email protected]');     // always succeeds (never reveals whether an email exists)

// when the page is opened from a reset email:
const pending = kumo.auth.pendingAction();                // { type: 'reset_password', token } or null
if (pending) await kumo.auth.resetPassword(pending.token, newPassword);

Verification links last 24 hours and reset links 30 minutes; each works once. A password reset signs the user out everywhere. kumo.auth.player.emailVerified tells you whether the email was confirmed.

Data export and account deletion#

App stores require in-app account deletion and privacy laws give users a right to a copy of their data. Put both in your settings screen:

const data = await kumo.account.exportData();       // everything stored about this player, as JSON (no password hashes)
await kumo.account.delete({ password });            // irreversible; guests confirm with their session instead

Deleting removes the user's data. auth.exportData() and auth.deleteAccount() are the same functions. You can also export or delete a user from your server (/v1/admin/players/:playerId/export, /v1/admin/players/:playerId/delete) or the dashboard.

Banning players#

Ban a user who cheats or harasses others. The ban signs them out on every device, closes their room connections at once and refuses their sign-ins and writes. Banning always works: there is no feature to turn on.

FromHow
DashboardPlayers → the user → Ban player / Unban player
AI agent (MCP)player_ban / player_unban (asks you to confirm every time)
Your server or Functions (secret key)POST /v1/admin/players/:playerId/ban { reason?, durationHours? } · POST /v1/admin/players/:playerId/unban {}

From your server or Functions, durationHours (more than 0, up to 8784 = 366 days) makes the ban end by itself; leave it out and the ban lasts until you lift it. reason (up to 500 characters) is for you: it shows in the dashboard and the audit log. The answer is { player: { id, banned, bannedAt, banReason, revokedSessions, banExpiresAt } }. An unknown user, or one from another environment, is 404 player_not_found. Publishable keys cannot ban.

There is only one ban per user: a ban set in any of these places can be lifted from any other. Your code can ban a cheater the moment it catches one; in the Functions starter, players.ban() / players.unban() in src/kumo.ts do the call for you (Functions).

### Ban appeals

A banned user can ask you to look at the ban again, once per ban. Appeals are part of banning, so they are always available. In your app, read the ban and send the appeal:

const { sanctions } = await kumo.sanctions.mine();
const ban = sanctions.find((s) => s.kind === 'ban' && s.active);
if (ban && !ban.appeal) await kumo.sanctions.appeal(ban.id, message); // one appeal per ban, up to 2000 characters

The banned user can read their bans with kumo.sanctions.mine() (GET /v1/players/me/sanctions) while their session is still open, up to 15 minutes after the ban, so show the appeal form then. On a later visit the SDK still sends the appeal: it keeps the refresh token the ban revoked and uses it as proof (POST /v1/auth/appeal). If the app no longer knows the ban's id, call kumo.sanctions.appeal(null, message): the appeal goes to the ban that revoked that token.

You answer each ban appeal in the dashboard (Players → Ban appeals: accept and unban, or reject, with an optional short reply), with your AI agent (appeals_list / appeal_resolve, asks you to confirm every time), or from your server (GET /v1/admin/appeals?status=open · POST /v1/admin/appeals/:appealId/resolve { accept, response? }). Afterwards the ban's appeal shows status: 'accepted' (the ban was lifted) or 'rejected', with your reply in response.

Server-side checks#

If your own server (or your Functions) needs to know who a user is, have the page send its access token (Authorization: Bearer <token>, from await kumo.auth.getAccessToken()), then ask KUMODeck from your server with your environment's secret key:

POST /v1/admin/players/verify-token
Authorization: Bearer sk_…
{ "token": "<the user's access token>" }
→ { "player": { "id", "displayName", "banned" } }

A token from another environment, or an expired one, is 401 token_expired (the page refreshes and retries). Check banned yourself: a banned user still verifies, so your code decides what they may do. The Functions starter wraps this as players.verify() in src/kumo.ts (with a 30-second cache), and the requireUser() helper in the functions-d1 Skill answers 401 / 403 for you (Functions). Keep the secret key on the server; never send it to the page. See the REST API.