@idosgames/mcp 0.1.36 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/registry/host.json +5 -5
- package/registry/index.json +33 -33
- package/registry/modules/ai.json +1 -1
- package/registry/modules/board-game.json +1 -1
- package/registry/modules/boosts.json +1 -1
- package/registry/modules/character.json +1 -1
- package/registry/modules/chat.json +1 -1
- package/registry/modules/collections.json +1 -1
- package/registry/modules/coop-events.json +1 -1
- package/registry/modules/craft.json +1 -1
- package/registry/modules/deals.json +1 -1
- package/registry/modules/events.json +1 -1
- package/registry/modules/friends.json +1 -1
- package/registry/modules/idle-rpg.json +1 -1
- package/registry/modules/inventory.json +1 -1
- package/registry/modules/invite.json +1 -1
- package/registry/modules/leaderboard.json +1 -1
- package/registry/modules/lootboxes.json +1 -1
- package/registry/modules/mailbox.json +1 -1
- package/registry/modules/marketplace.json +1 -1
- package/registry/modules/mini-game.json +1 -1
- package/registry/modules/multiplayer.json +1 -1
- package/registry/modules/premium.json +1 -1
- package/registry/modules/profile.json +1 -1
- package/registry/modules/pvp.json +1 -1
- package/registry/modules/quests.json +1 -1
- package/registry/modules/rewards.json +1 -1
- package/registry/modules/season.json +1 -1
- package/registry/modules/store.json +1 -1
- package/registry/modules/tutorials.json +1 -1
- package/registry/modules/voxelcraft.json +1 -1
- package/registry/modules/workshop.json +1 -1
- package/registry/skills/authentication.json +1 -1
package/dist/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idosgames/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"mcpName": "com.idosgames/sdk",
|
|
5
5
|
"description": "MCP server that serves the iDosGames Module & Skills Registry to AI coding agents (Claude Code, Codex, Cursor…): list/pull composable game modules and the host scaffold, and load skills for @idosgames/core, the module contract, and composition.",
|
|
6
6
|
"type": "module",
|
package/registry/host.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "package.json",
|
|
26
|
-
"content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (both from @idosgames/app-shell) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.14.
|
|
26
|
+
"content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (both from @idosgames/app-shell) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.14.1\",\n \"@idosgames/core\": \"0.22.1\",\n \"@idosgames/module-sdk\": \"0.12.0\",\n \"@idosgames/react\": \"0.5.2\",\n \"@idosgames/wallet\": \"0.6.5\",\n \"@reown/appkit\": \"1.8.23\",\n \"@reown/appkit-adapter-wagmi\": \"1.8.23\",\n \"@tanstack/react-query\": \"5.101.2\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\",\n \"wagmi\": \"3.7.2\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"6.0.4\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"8.1.5\"\n }\n}\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "public/sw.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"path": "src/app.config.ts",
|
|
34
|
-
"content": "import { defineAppConfig } from \"@idosgames/app-shell\";\n\n// What happens after sign-in and how the lobby is ordered. The look, animations and sounds are in\n// ./ui.config.ts. Which systems (shop, heroes, leaderboards…) and games the app has is\n// ./modules.ts: remove a module there and its tab is gone. A tab whose system has nothing configured\n// on the title yet (no store offers, no heroes…) stays hidden from players by itself.\n//\n// Everything is optional; uncomment and edit.\nexport default defineAppConfig({\n // \"game\" — sign-in → straight into a game that lives inside the lobby (idle-rpg); an app whose\n // games take the whole screen (voxelcraft, board-game) starts in the lobby. \"lobby\" — always\n // sign-in → lobby → \"Play\". The mode id of a game (\"voxelcraft\") starts in that one.\n start: \"game\",\n lobby: {\n // order: [\"play\", \"store\", \"character\"], // these tabs first, in this order\n // primary: [\"season\"], // also a main tab rather than under \"More\"\n },\n auth: {\n // The \"Play as guest\" button on the sign-in screen is OFF by default; uncomment to show it.\n // guest: true,\n },\n});\n"
|
|
34
|
+
"content": "import { defineAppConfig } from \"@idosgames/app-shell\";\n\n// What happens after sign-in and how the lobby is ordered. The look, animations and sounds are in\n// ./ui.config.ts. Which systems (shop, heroes, leaderboards…) and games the app has is\n// ./modules.ts: remove a module there and its tab is gone. A tab whose system has nothing configured\n// on the title yet (no store offers, no heroes…) stays hidden from players by itself.\n//\n// Everything is optional; uncomment and edit.\nexport default defineAppConfig({\n // \"game\" — sign-in → straight into a game that lives inside the lobby (idle-rpg); an app whose\n // games take the whole screen (voxelcraft, board-game) starts in the lobby. \"lobby\" — always\n // sign-in → lobby → \"Play\". The mode id of a game (\"voxelcraft\") starts in that one.\n start: \"game\",\n lobby: {\n // order: [\"play\", \"store\", \"character\"], // these tabs first, in this order\n // primary: [\"season\"], // also a main tab rather than under \"More\"\n },\n auth: {\n // The \"Play as guest\" button on the sign-in screen is OFF by default; uncomment to show it.\n // guest: true,\n // On the idosgames.com game page the player enters with their site account by itself (not signed\n // in there — the site shows its sign-in window). On by default; uncomment to keep this screen.\n // platformSignIn: false,\n },\n});\n"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "src/config.ts",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "src/idos.title.ts",
|
|
46
|
-
"content": "// The project's IDENTITY file — the ONE centralized place the Title id lives.\n//\n// On platform-created projects the platform GENERATES this file when it creates the project; there\n// the AI editor is denied write access to this path on purpose — do not edit it by hand. On a\n// manually scaffolded project (get_host_scaffold / copied template) there is no generator: YOU fill\n// IDOS_TITLE_ID here yourself. Either way, do not import anything into this file, and do not bind\n// the title anywhere else (.env.local is a local-dev fallback for the raw template only).\n//\n// It is the highest-priority source of the Title id (see config.ts for the full chain). It is baked\n// into the bundle rather than read from the URL because a build has to keep working where there is\n// no URL to read — packaged as a mobile app, embedded in an iframe, or opened from a shared link\n// that dropped its query string.\n//\n// It holds the CANONICAL (production) title. The DEV title is derived from it, never stored here —\n// one project has one identity, and DEV/PROD is an environment on top of that identity.\n\n/** Canonical (production) Title id. Empty only in the raw template, before the platform seeds it. */\nexport const IDOS_TITLE_ID = \"\";\n\n/** Build key for this title, if the title enforces one. */\nexport const IDOS_BUILD_KEY = \"\";\n\n/** Environment this artifact defaults to. Web builds may override it (see config.ts); a packaged\n * mobile build cannot, so a DEV app is produced by generating this file with \"dev\". */\nexport const IDOS_DEFAULT_ENV: \"prod\" | \"dev\" = \"prod\";\n\n/**\n * Whether this title was created as web3. Set by the platform from the same toggle the publisher\n * used at title creation.\n *\n * It
|
|
46
|
+
"content": "// The project's IDENTITY file — the ONE centralized place the Title id lives.\n//\n// On platform-created projects the platform GENERATES this file when it creates the project; there\n// the AI editor is denied write access to this path on purpose — do not edit it by hand. On a\n// manually scaffolded project (get_host_scaffold / copied template) there is no generator: YOU fill\n// IDOS_TITLE_ID here yourself. Either way, do not import anything into this file, and do not bind\n// the title anywhere else (.env.local is a local-dev fallback for the raw template only).\n//\n// It is the highest-priority source of the Title id (see config.ts for the full chain). It is baked\n// into the bundle rather than read from the URL because a build has to keep working where there is\n// no URL to read — packaged as a mobile app, embedded in an iframe, or opened from a shared link\n// that dropped its query string.\n//\n// It holds the CANONICAL (production) title. The DEV title is derived from it, never stored here —\n// one project has one identity, and DEV/PROD is an environment on top of that identity.\n\n/** Canonical (production) Title id. Empty only in the raw template, before the platform seeds it. */\nexport const IDOS_TITLE_ID = \"\";\n\n/** Build key for this title, if the title enforces one. */\nexport const IDOS_BUILD_KEY = \"\";\n\n/** Environment this artifact defaults to. Web builds may override it (see config.ts); a packaged\n * mobile build cannot, so a DEV app is produced by generating this file with \"dev\". */\nexport const IDOS_DEFAULT_ENV: \"prod\" | \"dev\" = \"prod\";\n\n/**\n * Whether this title was created as web3. Set by the platform from the same toggle the publisher\n * used at title creation.\n *\n * It does not gate the login screen — the title's settings decide whether the wallet button is\n * shown (see walletLogin.tsx).\n * Kept as baked metadata for code that wants to know the title's web3 origin BEFORE there is a\n * session (`client.title.getTitlePublicConfiguration()` requires one).\n */\nexport const IDOS_WEB3 = false;\n\n/**\n * NetworkID the wallet login challenge is issued for (e.g. \"bsc\", \"base\", \"solana\"). Empty on\n * web2 titles. Baked for the same reason as everything else here: the login screen needs it before\n * there is a session, and the title's blockchain config is only readable once logged in.\n */\nexport const IDOS_WEB3_NETWORK_ID = \"\";\n\n/**\n * WalletConnect Cloud / Reown project id (dashboard.reown.com). Enables MOBILE wallet login via\n * WalletConnect (QR on desktop, deep-link on phones); without it only browser-extension wallets\n * are offered and mobile login is unavailable. Set per-title in the blockchain config\n * (Blockchain.WalletLogin.WalletConnectProjectId) — the platform bakes it here. Baked, not read at\n * runtime, for the same reason as IDOS_WEB3_NETWORK_ID: the login screen runs before there is a\n * session, and the title's blockchain config is only readable once logged in.\n */\nexport const IDOS_WEB3_WALLETCONNECT_PROJECT_ID = \"\";\n"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "src/main.tsx",
|
|
50
|
-
"content": "// ПЕРВЫЙ импорт сознательно: зонд превью (из SDK, отдельная точка входа) ставит перехват console/ошибок\n// при загрузке своего модуля, и всё, что упадёт ниже по старту (включая config.ts на нераспознанном\n// тайтле), уже попадёт в его лог — а значит доедет до AI-кодера. Вне превью не делает ничего.\nimport { installPreviewProbe } from \"@idosgames/app-shell/preview-probe\";\nimport { createIDosGamesClient } from \"@idosgames/core\";\nimport {\n DefaultLoginScreen,\n DefaultPlayAccessScreen,\n createBaseModule,\n mountHost,\n} from \"@idosgames/app-shell\";\nimport appConfig from \"./app.config\";\nimport { modules } from \"./modules\";\nimport { TITLE_ID, BUILD_KEY } from \"./config\";\nimport { ENV_GOOGLE_CLIENT_ID, IS_DEV } from \"./env\";\nimport { IDOS_WEB3, IDOS_WEB3_NETWORK_ID } from \"./idos.title\";\nimport { renderWalletLink, renderWalletLogin } from \"./walletLogin\";\nimport { WalletButton, WalletTopUp } from \"./walletButton\";\nimport ui from \"./ui.config\";\n\n// Тайтл зонду — отдельным вызовом: сам он встал раньше, чем config.ts успел его разрешить.\ninstallPreviewProbe({ titleId: TITLE_ID });\n\nconst app = document.getElementById(\"app\");\nif (!app) throw new Error(\"#app container not found\");\n\n// Always runs against the real backend (https://api.idosgames.com) via the global fetch.\nconst client = createIDosGamesClient({\n titleID: TITLE_ID,\n buildKey: BUILD_KEY.length > 0 ? BUILD_KEY : undefined,\n throttleMs: 0,\n});\n\nclient.on(\"error:global\", (message) => {\n console.error(\"[idos] global error:\", message);\n});\nclient.on(\"error:connection\", (message) => {\n console.error(\"[idos] connection error:\", message);\n});\n\nif (IS_DEV) {\n (\n globalThis as typeof globalThis & { idosClient?: typeof client }\n ).idosClient = client;\n}\n\n// The host owns sign-in: mountHost replays the previous session (autoLogin) and, when there is\n// none, renders the login screen. Do NOT log in here — that would skip the screen, and with it the\n// player's ability to pick a provider or switch accounts.\n//\n// The app = the base (sign-in and the lobby, both from @idosgames/app-shell) + the modules of\n// ./modules.ts (the shop, heroes, leaderboards… and the games). The base goes first; what happens\n// after sign-in and the order of the lobby's tabs is ./app.config.ts, the look is ./ui.config.ts.\n//\n// The wallet stack is the project's (the base knows nothing about it): the lobby's wallet button is\n// ./walletButton (with the top-up window for a purchase short on crypto), sign-in / \"link wallet\"\n// buttons are ./walletLogin.\nmountHost({\n container: app,\n client,\n modules: [\n createBaseModule(appConfig, {\n web3: IDOS_WEB3,\n networkID: IDOS_WEB3_NETWORK_ID,\n WalletButton,\n WalletTopUp,\n }),\n ...modules,\n ],\n // The ready-made sign-in: wallet, iDos Games, Google, Apple, Telegram, e-mail, and the title's login\n // mode. \"Play as guest\" is off until `auth.guest` in ./app.config.ts turns it on; a Google client\n // id for local development is VITE_IDOS_GOOGLE_CLIENT_ID (a published game shows Google only when\n // the publisher set up the title's own Google sign-in). A game that needs another layout renders\n // its own screen here and keeps calling client.auth.login*() and props.onAuthenticated().\n renderLogin: (props) => (\n <DefaultLoginScreen\n {...props}\n renderWalletLogin={renderWalletLogin}\n allowGuest={appConfig.auth.guest}\n googleClientId={ENV_GOOGLE_CLIENT_ID}\n />\n ),\n // Shown instead of the app when the title's login mode requires a token balance the player has\n // not confirmed yet: it lists the requirements, the linked wallets, \"Link wallet\" and \"Check\".\n renderPlayAccess: (props) => (\n <DefaultPlayAccessScreen {...props} renderLinkWallet={renderWalletLink} />\n ),\n // Look, animations and sounds of every module's interface — edit ./ui.config.ts, not the modules.\n ui,\n});\n"
|
|
50
|
+
"content": "// ПЕРВЫЙ импорт сознательно: зонд превью (из SDK, отдельная точка входа) ставит перехват console/ошибок\n// при загрузке своего модуля, и всё, что упадёт ниже по старту (включая config.ts на нераспознанном\n// тайтле), уже попадёт в его лог — а значит доедет до AI-кодера. Вне превью не делает ничего.\nimport { installPreviewProbe } from \"@idosgames/app-shell/preview-probe\";\nimport { createIDosGamesClient } from \"@idosgames/core\";\nimport {\n DefaultLoginScreen,\n DefaultPlayAccessScreen,\n createBaseModule,\n mountHost,\n} from \"@idosgames/app-shell\";\nimport appConfig from \"./app.config\";\nimport { modules } from \"./modules\";\nimport { TITLE_ID, BUILD_KEY } from \"./config\";\nimport { ENV_GOOGLE_CLIENT_ID, IS_DEV } from \"./env\";\nimport { IDOS_WEB3, IDOS_WEB3_NETWORK_ID } from \"./idos.title\";\nimport { renderWalletLink, renderWalletLogin } from \"./walletLogin\";\nimport { WalletButton, WalletTopUp } from \"./walletButton\";\nimport ui from \"./ui.config\";\n\n// Тайтл зонду — отдельным вызовом: сам он встал раньше, чем config.ts успел его разрешить.\ninstallPreviewProbe({ titleId: TITLE_ID });\n\nconst app = document.getElementById(\"app\");\nif (!app) throw new Error(\"#app container not found\");\n\n// Always runs against the real backend (https://api.idosgames.com) via the global fetch.\nconst client = createIDosGamesClient({\n titleID: TITLE_ID,\n buildKey: BUILD_KEY.length > 0 ? BUILD_KEY : undefined,\n throttleMs: 0,\n});\n\nclient.on(\"error:global\", (message) => {\n console.error(\"[idos] global error:\", message);\n});\nclient.on(\"error:connection\", (message) => {\n console.error(\"[idos] connection error:\", message);\n});\n\nif (IS_DEV) {\n (\n globalThis as typeof globalThis & { idosClient?: typeof client }\n ).idosClient = client;\n}\n\n// The host owns sign-in: mountHost replays the previous session (autoLogin) and, when there is\n// none, renders the login screen. Do NOT log in here — that would skip the screen, and with it the\n// player's ability to pick a provider or switch accounts.\n//\n// The app = the base (sign-in and the lobby, both from @idosgames/app-shell) + the modules of\n// ./modules.ts (the shop, heroes, leaderboards… and the games). The base goes first; what happens\n// after sign-in and the order of the lobby's tabs is ./app.config.ts, the look is ./ui.config.ts.\n//\n// The wallet stack is the project's (the base knows nothing about it): the lobby's wallet button is\n// ./walletButton (with the top-up window for a purchase short on crypto), sign-in / \"link wallet\"\n// buttons are ./walletLogin.\nmountHost({\n container: app,\n client,\n modules: [\n createBaseModule(appConfig, {\n web3: IDOS_WEB3,\n networkID: IDOS_WEB3_NETWORK_ID,\n WalletButton,\n WalletTopUp,\n }),\n ...modules,\n ],\n // The ready-made sign-in: wallet, iDos Games, Google, Apple, Telegram, e-mail, and the title's login\n // mode. \"Play as guest\" is off until `auth.guest` in ./app.config.ts turns it on; a Google client\n // id for local development is VITE_IDOS_GOOGLE_CLIENT_ID (a published game shows Google only when\n // the publisher set up the title's own Google sign-in). A game that needs another layout renders\n // its own screen here and keeps calling client.auth.login*() and props.onAuthenticated().\n renderLogin: (props) => (\n <DefaultLoginScreen\n {...props}\n renderWalletLogin={renderWalletLogin}\n allowGuest={appConfig.auth.guest}\n googleClientId={ENV_GOOGLE_CLIENT_ID}\n />\n ),\n // Shown instead of the app when the title's login mode requires a token balance the player has\n // not confirmed yet: it lists the requirements, the linked wallets, \"Link wallet\" and \"Check\".\n renderPlayAccess: (props) => (\n <DefaultPlayAccessScreen {...props} renderLinkWallet={renderWalletLink} />\n ),\n // Look, animations and sounds of every module's interface — edit ./ui.config.ts, not the modules.\n ui,\n // On the idosgames.com game page the player enters with their site account by itself.\n platformSignIn: appConfig.auth.platformSignIn,\n});\n"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"path": "src/modules.ts",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"path": "src/walletLogin.tsx",
|
|
70
|
-
"content": "import { LazyWalletLogin } from \"@idosgames/wallet/react/lazy\";\nimport type { RenderLinkWallet, RenderWalletLogin } from \"@idosgames/app-shell\";\nimport { ENV_WALLETCONNECT_PROJECT_ID } from \"./env\";\nimport {\n IDOS_WEB3_NETWORK_ID,\n IDOS_WEB3_WALLETCONNECT_PROJECT_ID,\n} from \"./idos.title\";\n\n// Wallet sign-in seam
|
|
70
|
+
"content": "import { LazyWalletLogin } from \"@idosgames/wallet/react/lazy\";\nimport type { RenderLinkWallet, RenderWalletLogin } from \"@idosgames/app-shell\";\nimport { ENV_WALLETCONNECT_PROJECT_ID } from \"./env\";\nimport {\n IDOS_WEB3_NETWORK_ID,\n IDOS_WEB3_WALLETCONNECT_PROJECT_ID,\n} from \"./idos.title\";\n\n// Wallet sign-in seam. The project always supplies the button, but the sign-in screen SHOWS it only\n// when the title wants it: a token-gated login mode (Blockchain.PlayAccess) or the publisher's switch\n// Blockchain.WalletLogin.ShowOnLoginScreen — both are title settings, changed without a rebuild. An\n// open game does not offer a wallet at sign-in.\n//\n// The button uses the SDK's default EVM chain set (no `chains` prop). The challenge network comes\n// from the title (IDOS_WEB3_NETWORK_ID, baked by the platform); the \"bsc\" fallback keeps the button\n// alive on titles that did not pick a network at creation. A title whose Blockchain config is empty\n// will surface the server's error under the button — fix that by configuring the network (backend\n// MCP: get_blockchain / save_blockchain), not by hiding the button. To restrict the chains, import\n// them from this same subpath (mainnet, bsc, polygon, base, arbitrum, optimism, sepolia,\n// polygonAmoy — plain objects, safe to import) and pass `chains={[…]}`.\n//\n// For a title created with the web3 toggle the platform regenerates this file with the title's own\n// network. Keeping the seam in its own file is what makes that swap (and your own edits) safe.\n//\n// The import is \"@idosgames/wallet/react/lazy\", NOT \"@idosgames/wallet/react\". The wallet stack\n// (Reown AppKit + wagmi/viem) is megabytes of code most players never need; the lazy entry keeps it\n// out of the first load until the player actually taps sign-in.\n//\n// walletConnectProjectId adds MOBILE wallets via the WalletConnect QR/deep-link modal; without it\n// the button only works with injected wallets (MetaMask & co) and mobile login is unavailable. It\n// comes from the title's blockchain config (IDOS_WEB3_WALLETCONNECT_PROJECT_ID, baked by the\n// platform); VITE_WALLETCONNECT_PROJECT_ID in .env.local is a local-dev fallback.\n//\n// No `chains` is passed on purpose: the button falls back to the SDK's default EVM chain set, which\n// is the SAME list the in-game wallet panel uses. That shared list is what lets the login and the\n// panel share one wallet config, so a wallet connected here stays connected in the game.\n\nexport const renderWalletLogin: RenderWalletLogin = ({\n client,\n onAuthenticated,\n disabled,\n style,\n}) => (\n <LazyWalletLogin\n client={client}\n networkID={IDOS_WEB3_NETWORK_ID || \"bsc\"}\n onAuthenticated={onAuthenticated}\n disabled={disabled}\n style={style}\n walletConnectProjectId={\n IDOS_WEB3_WALLETCONNECT_PROJECT_ID ||\n ENV_WALLETCONNECT_PROJECT_ID ||\n undefined\n }\n />\n);\n\n// \"Link wallet\" on the play-access screen (mounted in ./main.tsx) — the same button in link mode. When\n// the title's login mode requires a token balance, a player who signed in another way attaches a\n// wallet to the account by signature, and the server checks the balance against exactly that\n// wallet. `networkID` is the network a requirement needs; the title's own network otherwise.\nexport const renderWalletLink: RenderLinkWallet = ({\n client,\n networkID,\n onLinked,\n disabled,\n style,\n}) => (\n <LazyWalletLogin\n mode=\"link\"\n client={client}\n networkID={networkID || IDOS_WEB3_NETWORK_ID || \"bsc\"}\n onLinked={onLinked}\n disabled={disabled}\n style={style}\n walletConnectProjectId={\n IDOS_WEB3_WALLETCONNECT_PROJECT_ID ||\n ENV_WALLETCONNECT_PROJECT_ID ||\n undefined\n }\n />\n);\n"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"path": "tsconfig.json",
|
package/registry/index.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedFromCommit": "
|
|
2
|
+
"generatedFromCommit": "9702b0af0b1ce500c54e92241b3d44a3c12aabd0",
|
|
3
3
|
"runtimePackages": {
|
|
4
|
-
"@idosgames/core": "0.22.
|
|
4
|
+
"@idosgames/core": "0.22.1",
|
|
5
5
|
"@idosgames/wallet": "0.6.5",
|
|
6
6
|
"@idosgames/module-sdk": "0.12.0",
|
|
7
7
|
"@idosgames/react": "0.5.2",
|
|
8
|
-
"@idosgames/app-shell": "0.14.
|
|
8
|
+
"@idosgames/app-shell": "0.14.1"
|
|
9
9
|
},
|
|
10
10
|
"host": {
|
|
11
11
|
"id": "base",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"version": "0.1.2",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@idosgames/core": "0.22.
|
|
48
|
+
"@idosgames/core": "0.22.1",
|
|
49
49
|
"@idosgames/module-sdk": "0.12.0",
|
|
50
50
|
"@idosgames/react": "0.5.2",
|
|
51
51
|
"react": "19.2.7"
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"version": "0.1.2",
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@idosgames/core": "0.22.
|
|
85
|
+
"@idosgames/core": "0.22.1",
|
|
86
86
|
"@idosgames/module-sdk": "0.12.0",
|
|
87
87
|
"@idosgames/react": "0.5.2",
|
|
88
88
|
"react": "19.2.7"
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
142
|
"dependencies": {
|
|
143
|
-
"@idosgames/core": "0.22.
|
|
143
|
+
"@idosgames/core": "0.22.1",
|
|
144
144
|
"@idosgames/module-sdk": "0.12.0",
|
|
145
145
|
"@idosgames/react": "0.5.2",
|
|
146
146
|
"react": "19.2.7"
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
},
|
|
178
178
|
"version": "0.1.1",
|
|
179
179
|
"dependencies": {
|
|
180
|
-
"@idosgames/core": "0.22.
|
|
180
|
+
"@idosgames/core": "0.22.1",
|
|
181
181
|
"@idosgames/module-sdk": "0.12.0",
|
|
182
182
|
"@idosgames/react": "0.5.2",
|
|
183
183
|
"react": "19.2.7"
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
},
|
|
215
215
|
"version": "0.1.2",
|
|
216
216
|
"dependencies": {
|
|
217
|
-
"@idosgames/core": "0.22.
|
|
217
|
+
"@idosgames/core": "0.22.1",
|
|
218
218
|
"@idosgames/module-sdk": "0.12.0",
|
|
219
219
|
"@idosgames/react": "0.5.2",
|
|
220
220
|
"react": "19.2.7"
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
},
|
|
251
251
|
"version": "0.1.1",
|
|
252
252
|
"dependencies": {
|
|
253
|
-
"@idosgames/core": "0.22.
|
|
253
|
+
"@idosgames/core": "0.22.1",
|
|
254
254
|
"@idosgames/module-sdk": "0.12.0",
|
|
255
255
|
"@idosgames/react": "0.5.2",
|
|
256
256
|
"react": "19.2.7"
|
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
},
|
|
292
292
|
"version": "0.1.3",
|
|
293
293
|
"dependencies": {
|
|
294
|
-
"@idosgames/core": "0.22.
|
|
294
|
+
"@idosgames/core": "0.22.1",
|
|
295
295
|
"@idosgames/module-sdk": "0.12.0",
|
|
296
296
|
"@idosgames/react": "0.5.2",
|
|
297
297
|
"react": "19.2.7"
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
},
|
|
328
328
|
"version": "0.1.2",
|
|
329
329
|
"dependencies": {
|
|
330
|
-
"@idosgames/core": "0.22.
|
|
330
|
+
"@idosgames/core": "0.22.1",
|
|
331
331
|
"@idosgames/module-sdk": "0.12.0",
|
|
332
332
|
"@idosgames/react": "0.5.2",
|
|
333
333
|
"react": "19.2.7"
|
|
@@ -366,7 +366,7 @@
|
|
|
366
366
|
},
|
|
367
367
|
"version": "0.1.2",
|
|
368
368
|
"dependencies": {
|
|
369
|
-
"@idosgames/core": "0.22.
|
|
369
|
+
"@idosgames/core": "0.22.1",
|
|
370
370
|
"@idosgames/module-sdk": "0.12.0",
|
|
371
371
|
"@idosgames/react": "0.5.2",
|
|
372
372
|
"react": "19.2.7"
|
|
@@ -405,7 +405,7 @@
|
|
|
405
405
|
},
|
|
406
406
|
"version": "0.1.2",
|
|
407
407
|
"dependencies": {
|
|
408
|
-
"@idosgames/core": "0.22.
|
|
408
|
+
"@idosgames/core": "0.22.1",
|
|
409
409
|
"@idosgames/module-sdk": "0.12.0",
|
|
410
410
|
"@idosgames/react": "0.5.2",
|
|
411
411
|
"react": "19.2.7"
|
|
@@ -445,7 +445,7 @@
|
|
|
445
445
|
},
|
|
446
446
|
"version": "0.1.2",
|
|
447
447
|
"dependencies": {
|
|
448
|
-
"@idosgames/core": "0.22.
|
|
448
|
+
"@idosgames/core": "0.22.1",
|
|
449
449
|
"@idosgames/module-sdk": "0.12.0",
|
|
450
450
|
"@idosgames/react": "0.5.2",
|
|
451
451
|
"react": "19.2.7"
|
|
@@ -486,7 +486,7 @@
|
|
|
486
486
|
},
|
|
487
487
|
"version": "0.1.2",
|
|
488
488
|
"dependencies": {
|
|
489
|
-
"@idosgames/core": "0.22.
|
|
489
|
+
"@idosgames/core": "0.22.1",
|
|
490
490
|
"@idosgames/module-sdk": "0.12.0",
|
|
491
491
|
"@idosgames/react": "0.5.2",
|
|
492
492
|
"react": "19.2.7"
|
|
@@ -525,7 +525,7 @@
|
|
|
525
525
|
},
|
|
526
526
|
"version": "0.1.2",
|
|
527
527
|
"dependencies": {
|
|
528
|
-
"@idosgames/core": "0.22.
|
|
528
|
+
"@idosgames/core": "0.22.1",
|
|
529
529
|
"@idosgames/module-sdk": "0.12.0",
|
|
530
530
|
"@idosgames/react": "0.5.2",
|
|
531
531
|
"react": "19.2.7"
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
},
|
|
568
568
|
"version": "0.1.3",
|
|
569
569
|
"dependencies": {
|
|
570
|
-
"@idosgames/core": "0.22.
|
|
570
|
+
"@idosgames/core": "0.22.1",
|
|
571
571
|
"@idosgames/module-sdk": "0.12.0",
|
|
572
572
|
"@idosgames/react": "0.5.2",
|
|
573
573
|
"react": "19.2.7"
|
|
@@ -604,7 +604,7 @@
|
|
|
604
604
|
},
|
|
605
605
|
"version": "0.1.2",
|
|
606
606
|
"dependencies": {
|
|
607
|
-
"@idosgames/core": "0.22.
|
|
607
|
+
"@idosgames/core": "0.22.1",
|
|
608
608
|
"@idosgames/module-sdk": "0.12.0",
|
|
609
609
|
"@idosgames/react": "0.5.2",
|
|
610
610
|
"react": "19.2.7"
|
|
@@ -645,7 +645,7 @@
|
|
|
645
645
|
},
|
|
646
646
|
"version": "0.1.2",
|
|
647
647
|
"dependencies": {
|
|
648
|
-
"@idosgames/core": "0.22.
|
|
648
|
+
"@idosgames/core": "0.22.1",
|
|
649
649
|
"@idosgames/module-sdk": "0.12.0",
|
|
650
650
|
"@idosgames/react": "0.5.2",
|
|
651
651
|
"react": "19.2.7"
|
|
@@ -685,7 +685,7 @@
|
|
|
685
685
|
},
|
|
686
686
|
"version": "0.1.2",
|
|
687
687
|
"dependencies": {
|
|
688
|
-
"@idosgames/core": "0.22.
|
|
688
|
+
"@idosgames/core": "0.22.1",
|
|
689
689
|
"@idosgames/module-sdk": "0.12.0",
|
|
690
690
|
"@idosgames/react": "0.5.2",
|
|
691
691
|
"react": "19.2.7"
|
|
@@ -725,7 +725,7 @@
|
|
|
725
725
|
},
|
|
726
726
|
"version": "0.1.2",
|
|
727
727
|
"dependencies": {
|
|
728
|
-
"@idosgames/core": "0.22.
|
|
728
|
+
"@idosgames/core": "0.22.1",
|
|
729
729
|
"@idosgames/module-sdk": "0.12.0",
|
|
730
730
|
"@idosgames/react": "0.5.2",
|
|
731
731
|
"react": "19.2.7"
|
|
@@ -765,7 +765,7 @@
|
|
|
765
765
|
},
|
|
766
766
|
"version": "0.1.2",
|
|
767
767
|
"dependencies": {
|
|
768
|
-
"@idosgames/core": "0.22.
|
|
768
|
+
"@idosgames/core": "0.22.1",
|
|
769
769
|
"@idosgames/module-sdk": "0.12.0",
|
|
770
770
|
"@idosgames/react": "0.5.2",
|
|
771
771
|
"react": "19.2.7"
|
|
@@ -806,7 +806,7 @@
|
|
|
806
806
|
},
|
|
807
807
|
"version": "0.1.1",
|
|
808
808
|
"dependencies": {
|
|
809
|
-
"@idosgames/core": "0.22.
|
|
809
|
+
"@idosgames/core": "0.22.1",
|
|
810
810
|
"@idosgames/module-sdk": "0.12.0",
|
|
811
811
|
"@idosgames/react": "0.5.2",
|
|
812
812
|
"react": "19.2.7"
|
|
@@ -844,7 +844,7 @@
|
|
|
844
844
|
},
|
|
845
845
|
"version": "0.1.1",
|
|
846
846
|
"dependencies": {
|
|
847
|
-
"@idosgames/core": "0.22.
|
|
847
|
+
"@idosgames/core": "0.22.1",
|
|
848
848
|
"@idosgames/module-sdk": "0.12.0",
|
|
849
849
|
"@idosgames/react": "0.5.2",
|
|
850
850
|
"react": "19.2.7"
|
|
@@ -888,7 +888,7 @@
|
|
|
888
888
|
},
|
|
889
889
|
"version": "0.2.0",
|
|
890
890
|
"dependencies": {
|
|
891
|
-
"@idosgames/core": "0.22.
|
|
891
|
+
"@idosgames/core": "0.22.1",
|
|
892
892
|
"@idosgames/module-sdk": "0.12.0",
|
|
893
893
|
"@idosgames/react": "0.5.2",
|
|
894
894
|
"react": "19.2.7"
|
|
@@ -927,7 +927,7 @@
|
|
|
927
927
|
},
|
|
928
928
|
"version": "0.1.4",
|
|
929
929
|
"dependencies": {
|
|
930
|
-
"@idosgames/core": "0.22.
|
|
930
|
+
"@idosgames/core": "0.22.1",
|
|
931
931
|
"@idosgames/module-sdk": "0.12.0",
|
|
932
932
|
"@idosgames/react": "0.5.2",
|
|
933
933
|
"react": "19.2.7"
|
|
@@ -964,7 +964,7 @@
|
|
|
964
964
|
},
|
|
965
965
|
"version": "0.1.2",
|
|
966
966
|
"dependencies": {
|
|
967
|
-
"@idosgames/core": "0.22.
|
|
967
|
+
"@idosgames/core": "0.22.1",
|
|
968
968
|
"@idosgames/module-sdk": "0.12.0",
|
|
969
969
|
"@idosgames/react": "0.5.2",
|
|
970
970
|
"react": "19.2.7"
|
|
@@ -1008,7 +1008,7 @@
|
|
|
1008
1008
|
},
|
|
1009
1009
|
"version": "0.1.3",
|
|
1010
1010
|
"dependencies": {
|
|
1011
|
-
"@idosgames/core": "0.22.
|
|
1011
|
+
"@idosgames/core": "0.22.1",
|
|
1012
1012
|
"@idosgames/module-sdk": "0.12.0",
|
|
1013
1013
|
"@idosgames/react": "0.5.2",
|
|
1014
1014
|
"react": "19.2.7"
|
|
@@ -1043,7 +1043,7 @@
|
|
|
1043
1043
|
},
|
|
1044
1044
|
"version": "0.1.2",
|
|
1045
1045
|
"dependencies": {
|
|
1046
|
-
"@idosgames/core": "0.22.
|
|
1046
|
+
"@idosgames/core": "0.22.1",
|
|
1047
1047
|
"@idosgames/module-sdk": "0.12.0",
|
|
1048
1048
|
"@idosgames/react": "0.5.2",
|
|
1049
1049
|
"react": "19.2.7"
|
|
@@ -1093,7 +1093,7 @@
|
|
|
1093
1093
|
]
|
|
1094
1094
|
},
|
|
1095
1095
|
"dependencies": {
|
|
1096
|
-
"@idosgames/core": "0.22.
|
|
1096
|
+
"@idosgames/core": "0.22.1",
|
|
1097
1097
|
"@idosgames/module-sdk": "0.12.0",
|
|
1098
1098
|
"@idosgames/react": "0.5.2",
|
|
1099
1099
|
"@idosgames/wallet": "0.6.5",
|
|
@@ -1175,7 +1175,7 @@
|
|
|
1175
1175
|
]
|
|
1176
1176
|
},
|
|
1177
1177
|
"dependencies": {
|
|
1178
|
-
"@idosgames/core": "0.22.
|
|
1178
|
+
"@idosgames/core": "0.22.1",
|
|
1179
1179
|
"@idosgames/module-sdk": "0.12.0",
|
|
1180
1180
|
"@idosgames/react": "0.5.2",
|
|
1181
1181
|
"phaser": "4.2.1",
|
|
@@ -1265,7 +1265,7 @@
|
|
|
1265
1265
|
},
|
|
1266
1266
|
"version": "0.7.0",
|
|
1267
1267
|
"dependencies": {
|
|
1268
|
-
"@idosgames/core": "0.22.
|
|
1268
|
+
"@idosgames/core": "0.22.1",
|
|
1269
1269
|
"@idosgames/module-sdk": "0.12.0",
|
|
1270
1270
|
"@idosgames/react": "0.5.2",
|
|
1271
1271
|
"react": "19.2.7",
|
|
@@ -1301,7 +1301,7 @@
|
|
|
1301
1301
|
},
|
|
1302
1302
|
"version": "0.1.2",
|
|
1303
1303
|
"dependencies": {
|
|
1304
|
-
"@idosgames/core": "0.22.
|
|
1304
|
+
"@idosgames/core": "0.22.1",
|
|
1305
1305
|
"@idosgames/module-sdk": "0.12.0",
|
|
1306
1306
|
"@idosgames/react": "0.5.2",
|
|
1307
1307
|
"react": "19.2.7"
|
package/registry/modules/ai.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "authentication",
|
|
3
3
|
"description": "Log players into a game on the iDosGames TypeScript SDK (@idosgames/core) via client.auth (AuthenticationService): guest/device-id login, two-step email registration with a confirmation code, email login, Google/Telegram login, SSO-code login from idosgames.com, wallet login, password reset, auto login on relaunch, session refresh, logout, and client-side email/password validation. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and asks about logging a player in, sessions, registration, email confirmation codes, resending a code, guest accounts, device-id login, Telegram login, Google login, SSO login, wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise touches client.auth, AuthenticationService, or AuthContext — even if they don't name the module explicitly.",
|
|
4
|
-
"content": "---\nname: authentication\ndescription: >-\n Log players into a game on the iDosGames TypeScript SDK (@idosgames/core)\n via client.auth (AuthenticationService): guest/device-id login, two-step\n email registration with a confirmation code, email login, Google/Telegram\n login, SSO-code login from idosgames.com, wallet login, password reset, auto\n login on relaunch, session refresh, logout, and client-side email/password\n validation. Use this whenever the user is working in the iDosGames TS SDK or\n its game templates (board-game, idle-rpg) and asks about logging a player\n in, sessions, registration, email confirmation codes, resending a code,\n guest accounts, device-id login, Telegram login, Google login, SSO login,\n wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise\n touches client.auth, AuthenticationService, or AuthContext — even if they\n don't name the module explicitly.\n---\n\n# Authentication (iDosGames TS SDK)\n\nThe Authentication module is how a player gets a session. It's the one module\nevery other feature depends on: nothing else on `client` works until a login\nmethod has succeeded. Unlike feature modules, most of its methods don't return\na narrow per-feature payload — they return the **entire post-login bootstrap\nstate** (`ClientState`: title config + the player's full `User` state),\nbecause a successful login is also \"give me everything the client needs to\nrender.\"\n\nThis skill is for **using** production login flows, not for porting or\nextending the service. If a login is rejected, that's the backend enforcing a\nrule (bad credentials, duplicate email, banned account) — surface the error,\ndon't try to reproduce the check client-side.\n\n## Remember me\n\n`setRememberSession(remember)` decides whether a successful login is written to storage. Call it\n**before** a `login*` method — it is read when that login completes, and it applies to whichever\nprovider runs next.\n\n```ts\nclient.auth.setRememberSession(rememberCheckbox); // default: true\nconst result = await client.auth.loginWithDeviceID();\n```\n\n- **On** (the default, and how every release before core 0.1.3 behaved) — the session is persisted,\n so `autoLogin()` signs the player back in on the next launch.\n- **Off** — nothing is persisted and any previously remembered session is dropped, so the next\n launch opens on the login screen. The current session is NOT weakened: the credentials stay in\n memory for this tab, so the transport's automatic 401 re-login still works.\n\n`logout()` ends the session and clears the persisted one, so a reload does not walk back into the\naccount the player just left. (Before core 0.1.3 it left storage untouched and a reload silently\nsigned them back in.)\n\nSessions are extended by the **refresh token** (`Authentication/RefreshSession`, core ≥ 0.18): every\nlogin returns one, it is kept in memory and — when remembering — in storage, and rotated on every\nrefresh. A 401 and a cold-start `autoLogin()` use it first, so ANY method comes back, wallet and\nGoogle included; replaying the login method is only the fallback for a missing/expired token. On a\ntitle whose login mode needs a token balance the server re-checks it on that refresh; a balance\nthat dropped closes the game (`client.auth.playAccess.Granted === false`), it does not end the session.\n\n## Mental model\n\n- **Guest login** (`loginWithDeviceID`) — anonymous account keyed off a\n per-device ID the platform adapter generates/stores. Zero-friction first\n launch; no credentials to lose, but also nothing to recover if the device ID\n is gone (reinstall, new device).\n- **Linked login methods** — `loginWithEmail` / `registerWithEmail`,\n `loginWithGoogle`, `loginWithTelegram`, `loginWithSsoCode`. These tie\n the account to a real identity so the player can resume it elsewhere.\n- **`autoLogin()`** picks up where the player left off. It first extends the\n remembered session by its **refresh token** (works for every method). Only\n without a usable token does it fall back to replaying — and that fallback\n covers just the methods that don't need a fresh externally-issued token: it replays `loginWithEmail`\n if the last successful login was Email (saved password), `loginWithDeviceID`\n if it was Device/None, and `loginWithTelegram` if it was Telegram (its\n `initData` is re-read live from the Telegram WebApp bridge each time, not\n stored). For Google/Facebook/GooglePlay/platform-token, there's no stored\n credential to replay — `autoLogin()` returns `reason: \"client\"` instead of\n guessing, because DeviceID and email/platform logins hash to **different\n backend UserIDs** (see Gotchas), so silently falling back to a guest login\n would strand the player on an unrelated empty account. What `autoLogin()`\n has to replay is controlled by **\"remember me\"** — see below. Call this on app\n start instead of hand-rolling \"which method did they use last,\" but be ready\n to handle its failure by re-running the platform's sign-in flow.\n- **Session refresh is automatic.** The HTTP transport calls\n `refreshSession()` itself when a request comes back 401 — it calls the\n server's `RefreshSession` with the refresh token (falling back to replaying\n the login method only without a usable token) and retries once. You almost never call\n `refreshSession()` directly; it's documented here for completeness, not as a\n method you wire up in app code.\n- **Static validators** (`AuthenticationService.isValidEmail`,\n `AuthenticationService.isValidPasswordLength`) are plain synchronous\n functions, not calls to the backend — use them to validate a form before\n spending a network round-trip on a login/register call that will fail\n server-side anyway. They mirror, but do not replace, the server's own\n checks — the server always re-validates.\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any other auth.* login method\n\nclient.auth.isLoggedIn; // true once a login call has succeeded\n```\n\nEvery other module's methods require this to have succeeded first — without a\nsession they return `{ ok: false, reason: \"unauthorized\" }` rather than\nthrowing. There is one `client` per player; don't share it across sessions.\nAuthentication requests carry no Bearer ticket at all — login/register/reset\nare the one family of calls that work before you have a session.\n\n## Methods\n\nAll login/register/reset methods return `Promise<OperationResult<T>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args, e.g. missing Telegram initData or empty platform token),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\nthrottle window), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe reason code the backend returned, e.g. `\"INCORRECT_EMAIL_OR_PASSWORD\"`,\n`\"INVALID_VERIFICATION_CODE\"` — see Gotchas for the verbatim set).\n\n| Method | Purpose | `data` on success |\n| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |\n| `loginWithDeviceID()` | Anonymous/guest login keyed off the device ID. | `ClientState` |\n| `loginWithTelegram()` | Login using the Telegram Mini App's init data. | `ClientState` |\n| `loginWithEmail(email, password)` | Login with email + password. | `ClientState` |\n| `registerWithEmail(email, password)` | **Start** a registration. Only mails a code — never returns a session, see below. | `EmailRegistrationOutcome` |\n| `confirmEmailRegistration(email, code)` | Finish the registration with the code from the email, creating the player. | `ClientState` |\n| `resendVerificationCode(email)` | Mail a fresh code. Resolves successfully whatever happened — except when there is no usable sender at all (`EMAIL_SENDER_NOT_CONFIGURED`). | `void` |\n| `requestWalletChallenge(networkID, address)` | Ask for the message the player's wallet must sign. Step one of wallet login. | `WalletChallengeResponse` |\n| `loginWithWallet(networkID, address, signature)` | Exchange the signed challenge for a title session. | `ClientState` |\n| `loginWithGoogle(googleIDToken)` | Login with a Google ID token. | `ClientState` |\n| `loginWithSsoCode(code)` | Exchange a one-time code from idosgames.com for a title session. **Web only** — see below. | `ClientState` |\n| `forgotPassword(email)` | Trigger a password-reset email (a 6-digit code). | `SuccessResponse` |\n| `resetPassword(email, resetToken, password)` | Complete a password reset using the emailed code. | `SuccessResponse` |\n| `autoLogin()` | Replay the last-used auth method (Email/Device/None/Telegram only — fails with `reason: \"client\"` for Google/Facebook/GooglePlay/platform-token). | `ClientState` |\n| `refreshSession()` | Re-authenticate and return a fresh session ticket; used internally by the transport on 401. | `string \\| null` (not an `OperationResult`) |\n| `logout()` | Clear the session, forget the saved login method, and reset the local cache. Synchronous, no network call. | `void` |\n| `setRememberSession(remember)` | \"Remember me\" — whether the NEXT login is persisted for `autoLogin()`. Default `true`. Call before a `login*` method. | `void` |\n| `AuthenticationService.isValidEmail(email)` | Static, synchronous client-side format check. | `boolean` |\n| `AuthenticationService.isValidPasswordLength(pw)` | Static, synchronous length check (8–100 chars). | `boolean` |\n\n⚠ **Each title has its OWN password.** The same address in two games means two independent\npasswords: resetting in one game does not change the login for the other. The reason is that the\nreset e-mail goes out through the PUBLISHER's SMTP — with one shared password, one publisher's mail\nserver would control access to every other publisher's game. So don't tell the player \"the same\npassword as in your other game\", and don't reuse stored credentials across titles.\nThe IDENTITY is still shared: Google, Apple and the idosgames.com sign-in recognise the same person\nin every game. Identity answers \"who is this\", not \"what proves they may sign in\".\n\n⚠ **Signing in through idosgames.com from a NATIVE build is not in this SDK.** `beginSsoRedirect` /\n`loginWithSsoCode` are the web half: the browser comes back to the same page with the code in the\nURL fragment. In a packaged Android/iOS build the browser is a separate program that tells the app\nnothing, so that flow uses a request the game polls for instead (`BeginNativeSsoRequest` /\n`PollNativeSsoRequest`), and it is implemented in the Unity SDK. Don't reach for `loginWithSsoCode`\nfrom a native wrapper — it has no way to receive the code.\n\n⚠ **That native flow now has a SHORT CODE, and a native client should use it.** `BeginNativeSsoRequest`\naccepts `UseUserCode: true` and then answers with `UserCode` (`WDJB-MJHT`) plus `VerificationUri`.\nThe game displays the code; the player opens the plain URL and types it there. The older shape — the\ngame opening `…/sso?title=X&request=<handle>` and the page confirming on its own — still works for\nalready-shipped builds, but **a request created with a code refuses that path**, so a build must do\none or the other, never a mix.\n\nThe reason is not convenience. In the older shape the handle travels IN THE ADDRESS, so the link can\nbe sent to somebody else: the attacker creates the request on their own device, the victim opens the\nlink and confirms, and the attacker's game gets a session as the victim. PKCE does not help (the\nverifier belongs to whoever created the request). With a code there is nothing in the URL to forward\n— the request is named only by what is on the device's own screen. `GetAuthOptions` reports\n`NativeSso.UserCodeSupported` and `NativeSso.PollIntervalSeconds`; ask before assuming, and follow\nthe interval or the poll quota will refuse you.\n\n⚠ **The code is shown, the secret is not.** Players read codes off screens, over shoulders and out\nof screenshots — so the code alone must never buy a session. Polling keeps requiring the request\nsecret, which never leaves the game.\n\nNon-Promise instance getters (read directly, no `await`):\n\n| Getter | Returns |\n| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `client.auth.context` | `AuthContext \\| null` — `{ userID, clientSessionTicket, clientSessionTicketExpiration, platformUserID?, platformAuthToken?, platformAuthTokenExpiration? }`, or `null` if never logged in. |\n| `client.auth.isLoggedIn` | `true` once `context` has a non-empty `userID` and `clientSessionTicket`. |\n| `client.auth.lastAuthType` | The persisted `AuthType` (`\"None\" \\| \"Device\" \\| \"Email\" \\| \"iDosGames\" \\| \"Facebook\" \\| \"Google\" \\| \"GooglePlay\" \\| \"Telegram\"`) from the previous successful login, read from local storage. |\n\nOn a successful login/register call, the SDK mirrors the full `ClientState`\ninto `client.data` (title config + user state) — same cache every other\nmodule reads from — and emits events. `email`/`password` are persisted to\nlocal storage **only** after a successful `loginWithEmail` or\n`confirmEmailRegistration` call, so `autoLogin()` has something to replay.\n⚠ Note it is the CONFIRMATION, not `registerWithEmail`, that persists them:\nregistration alone creates no account, so there is nothing to replay yet (the\npassword is carried over from the first step internally). A leftover\nusername you pass in is only a fallback — Google logins use the Google\nprofile name, platform-token logins use the platform profile name, and if\nnone is available anywhere the backend assigns a generated one (see Gotchas).\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn.\n\n- `auth:requestSent` → `void` — fired at the start of every login/reset call (before the network round-trip). One exception: `loginWithSsoCode` validates its argument first, so an empty-code `reason: \"client\"` failure doesn't fire it.\n- `auth:loggedIn` → `void` — fired after a login/register call succeeds and `ClientState` has been applied to the cache.\n- `auth:loggedOut` → `void` — fired synchronously by `logout()`.\n- `auth:unauthorized` → `void` — transport-level, fired by the HTTP layer only when a 401 could **not** be transparently recovered (the automatic refresh was unavailable, already attempted, or failed). A 401 that the refresh-and-retry rescues never emits it. Not emitted by `AuthenticationService` itself.\n- `user:clientStateReceived` → `ClientState` — fired whenever a full client state is applied (login, or any other flow that re-fetches it).\n- `user:stateUpdated` → `void` — fired whenever the cached `User` half of `ClientState` is replaced, which includes every successful login.\n- `user:anyUpdated` → `void` — coarsest \"something in the user cache changed\" signal; also fires on login.\n\n```ts\nconst off = client.on(\"auth:loggedIn\", () => {\n console.log(\"logged in as\", client.auth.context?.userID);\n});\n// later: off();\n```\n\n## Recipes\n\n### Guest login on first launch\n\nIn the base template the \"Play as guest\" button is OFF by default; a project turns it on with\n`auth: { guest: true }` in `src/app.config.ts`. The Google button shows only when the title has its\nOWN Google client (`getAuthOptions()` → `Google.Enabled` and not `Google.PlatformFallback`).\n\n```ts\nconst result = await client.auth.loginWithDeviceID();\nif (!result.ok) return showError(result.error ?? result.reason);\n// client.data now has the full ClientState; render the game.\n```\n\n### Try auto-login first, fall back appropriately\n\n```ts\nasync function bootstrap() {\n const result = await client.auth.autoLogin();\n if (result.ok) return; // resumed the last session\n if (result.reason === \"client\" && client.auth.lastAuthType !== \"None\") {\n // Last session used Google/Facebook/GooglePlay/a platform token — autoLogin\n // can't replay that silently. Re-run that platform's sign-in flow and call\n // the matching login method (e.g. loginWithGoogle) with a fresh token.\n return reauthenticateViaPlatformSDK(client.auth.lastAuthType);\n }\n showError(result.error ?? result.reason);\n}\n```\n\n`autoLogin()` safely self-replays Email, Device/None, and Telegram — call it\nonce on app start for those. For anything else it deliberately does **not**\nfall back to a guest login (see Gotchas) — treat its `reason: \"client\"`\nfailure as \"go get a fresh token,\" not as a generic error to toast.\n\n### Register a new account with email\n\n```ts\nif (!AuthenticationService.isValidEmail(email)) {\n return showFieldError(\"email\", \"Enter a valid email address.\");\n}\nif (!AuthenticationService.isValidPasswordLength(password)) {\n return showFieldError(\"password\", \"Password must be 8–100 characters.\");\n}\n\nconst started = await client.auth.registerWithEmail(email, password);\nif (!started.ok) return showError(started.error ?? started.reason);\n\n// No account yet — a code was mailed. Show a code screen; the session arrives from confirm.\nconst done = await client.auth.confirmEmailRegistration(email, codeFromPlayer);\nif (!done.ok) return showError(done.error ?? done.reason);\n```\n\n⚠ **`registerWithEmail` never creates an account, and success does not mean the address was\nfree.**\n\nRegistration always stores the attempt and mails a 6-digit code; the player is created by\n`confirmEmailRegistration`. Building a screen that assumes an immediate session will leave players\nstaring at a spinner that never resolves.\n\nConfirmation is **platform behaviour, not a title setting** — there is no flag to switch it off,\nand no `verificationRequired` branch to write. Only the numbers are configurable (code lifetime,\nattempt cap, resend cooldown).\n\nThe answer is also deliberately **identical for a free and for a taken address** — there is no\n`EMAIL_ALREADY_EXISTS` on this path any more. Telling them apart would turn the registration form\ninto a way of checking whether a given person has an account here, which is exactly what the\npassword-recovery form goes out of its way to avoid. The owner of a taken address gets a \"you\nalready have an account\" e-mail instead, without a code.\n\nOne failure on this path **is** reported: `EMAIL_SENDER_NOT_CONFIGURED`, when the server has no\nusable mail sender at all. It says nothing about the address — the same answer comes back for a\nfree one, a taken one and a typo — and it exists because confirmation cannot be switched off: with\nno sender, e-mail registration does not work at all, and answering \"code sent\" would hide a broken\ndeployment behind a normal-looking screen. Show it as a server-side problem, not as the player's\nmistake, and do not offer a \"resend\" — the resend refuses the same way. A letter that merely fails\nto leave is a different case and still answers success.\n\nEvery way of getting the code wrong — no such registration, expired, mismatched — answers with the\nsame `INVALID_VERIFICATION_CODE`, for the same reason. Do not branch on it.\n\n`resendVerificationCode` mails a **new** code: only its hash is stored, so the previous one cannot\nbe re-sent and stops working. Rate-limit the button in the UI (the server's cooldown is reported as\n`resendCooldownSeconds`) — the server answers successfully even when it sent nothing.\n\nEmail is matched case-insensitively — the backend trims and lowercases it\nbefore every lookup and write, so `Player@Mail.com` and `player@mail.com`\ncollide on the same account.\n\n### Upgrade a guest account to an email account\n\nThe SDK has no dedicated \"link email to this guest\" call — `registerWithEmail`\nalways mints its **own** account (a fresh UserID derived from the email, via a\ndifferent hash than the device-id account uses). To move a guest forward\nwithout losing their progress, do it through your own game-side flow (e.g. a\ncloud script that copies/merges state) rather than assuming registration\ncarries the guest's inventory along; don't build a UI that implies it will.\n\n### Forgot / reset password\n\n```ts\nconst sent = await client.auth.forgotPassword(email);\nif (!sent.ok) return showError(sent.error ?? sent.reason);\n// If an account exists for this email, a 6-digit reset code was emailed,\n// valid for 5 minutes. The response looks identical whether or not the\n// email is registered (see Gotchas) — don't tell the player \"no such account.\"\n\nconst reset = await client.auth.resetPassword(email, resetToken, newPassword);\nif (!reset.ok) return showError(reset.error ?? reset.reason);\n// password changed; call loginWithEmail with the new password next\n```\n\n### Handle a rejected login\n\n```ts\nconst result = await client.auth.loginWithEmail(email, password);\nif (!result.ok) {\n switch (result.reason) {\n case \"connection\":\n return offerRetry();\n case \"throttled\":\n return; // ignore — same call already in flight/just ran\n case \"client\":\n case \"server\":\n default:\n return showError(result.error ?? \"Login failed.\");\n }\n}\n```\n\n### Log out\n\n```ts\nclient.auth.logout(); // synchronous — clears context + resets client.data cache\n// route to login screen; no network call is made\n```\n\n## Gotchas\n\n- **A title's login mode can refuse methods** (`cfg.Blockchain.PlayAccess`). `WalletOnly` accepts\n only wallet login (others fail with `PLAY_ACCESS_WALLET_LOGIN_ONLY`); `AnyWithLinkedWallet` refuses\n guests (`PLAY_ACCESS_GUEST_DISABLED`) and lets other logins in with `client.auth.playAccess.Granted\n=== false` until a wallet is linked and holds enough tokens. Read the mode BEFORE login with\n `client.auth.getAuthOptions()` (`PlayAccessMode`) to hide dead buttons. The host template does all\n of this already — see the `blockchain-system` skill, \"Login mode and play access\".\n- **`autoLogin()` deliberately refuses to guess for federated/platform\n logins.** The backend derives DeviceID UserIDs from\n `SHA256(platform + device + deviceID)` and email/platform-linked UserIDs\n from `SHA256(email + titleID)` — two different hashes with no server-side\n link between them. Falling back to `loginWithDeviceID()` for a\n Google/Facebook/GooglePlay/platform-token session would silently resolve to\n a different, empty account, not \"safely resume as a guest.\" If\n `lastAuthType` is one of those, `autoLogin()` returns\n `{ ok: false, reason: \"client\" }` instead; the app must obtain a fresh token\n from that platform's SDK and call the matching `login*` method itself.\n- **The exact `error` strings on `reason: \"server\"` are backend-defined\n codes/messages**, not prose meant for direct display — surface them through\n your own copy/localization layer rather than showing them raw. Verified\n values from the backend for the flows above: `\"INVALID_INPUT_DATA\"`\n (missing/blank required field) and `\"INCORRECT_EMAIL_OR_PASSWORD\"` (bad\n credentials **or** unknown email — the backend intentionally doesn't\n distinguish the two, so don't tell the player \"no such account\") and\n `\"INVALID_VERIFICATION_CODE\"` (every way of getting the confirmation code\n wrong, collapsed into one) are shared\n `MessageCode` enum values also used elsewhere in the backend;\n `\"EMAIL_ALREADY_EXISTS\"` is **no longer returned by registration** — the form\n answers identically for a free and for a taken address, on purpose;\n `\"RATE_LIMIT_EXCEEDED\"` / `\"OPERATION_IN_PROGRESS\"` (per-account login lock\n — see below) are the same enum family. Google/platform-token logins instead\n return ad-hoc string literals specific to that flow, e.g. `\"BANNED_GLOBAL\"`\n (platform account is banned), `\"GOOGLE_ACCOUNT_CONFLICT\"` (email already\n linked to a _different_ Google account), `\"INVALID_GOOGLE_TOKEN\"`\n (bad/expired Google ID token) — see\n [references/data-model.md](references/data-model.md) for the full list.\n Telegram login instead surfaces plain sentences like `\"Invalid Telegram\ndata\"` / an internal `\"Telegram auth_date is stale\"` condition (initData\n older than 24h or timestamped implausibly in the future) — treat any\n non-uppercase-code string as an opaque message, not something to\n pattern-match on.\n- **Per-account login is rate-limited server-side, independent of the SDK's\n own 600 ms throttle.** Repeated `loginWithEmail` attempts for the same\n account inside a ~1-second window come back `\"RATE_LIMIT_EXCEEDED\"`; a login\n already being processed for that account comes back\n `\"OPERATION_IN_PROGRESS\"`. This is a brute-force guard, not a bug — don't\n retry-loop past it.\n- **Telegram login needs the platform adapter's init data.** `loginWithTelegram()`\n calls `platform.getTelegramInitDataRaw()` first; if that returns falsy (not\n running inside Telegram, or the adapter doesn't support it), the call fails\n client-side with `reason: \"client\"` before any network request — no point\n retrying without fixing the environment. Server-side, the init data is\n rejected if its `auth_date` is missing, more than 24 hours old, or more than\n 5 minutes in the future — a Telegram Mini App that's been idle a long time\n may need a fresh WebApp launch, not just a retry. On success it's recorded\n under its own `AuthType.Telegram` (not `Device`) precisely so `autoLogin()`\n replays `loginWithTelegram()` again rather than a device-id login.\n- **`loginWithPlatformToken` no longer exists.** The site used to hand games the\n platform auth token — a 24-hour string that also authorizes the whole\n management API. It was replaced by the one-time SSO code\n (`loginWithSsoCode`), and the endpoint has since been removed as unreachable.\n A session opened by that code is still recorded under `AuthType.iDosGames`,\n which is what `lastAuthType` reports afterward.\n- **A username you pass in is a last resort, not a guarantee.** For Google and\n platform-token logins, the backend prefers the identity provider's own\n profile name over any client-supplied username on every login (not just the\n first) — so a locally cached display name can be silently overwritten by the\n linked account's name. If no name is available from any source at\n registration time, the backend assigns a generated one\n (`\"BraveTiger482\"`-style: adjective + noun + 2–4 digit number) rather than\n leaving it blank.\n- **`forgotPassword` never reveals whether an email is registered.** Unknown\n emails and known emails both return `{ ok: true }` with no email actually\n sent for the unknown case — this is a deliberate anti-enumeration measure,\n not a bug to work around. The emailed reset code is a 6-digit number valid\n for 5 minutes; requesting again inside a 60-second cooldown silently\n no-ops (still returns `ok: true`, no second email). `resetPassword` allows\n at most 5 wrong-code attempts before the code is invalidated server-side and\n a fresh `forgotPassword` call is required.\n ⚠ **The remaining-attempt count is NOT in the response any more**, and neither\n is \"no such account\": every outcome of `resetPassword` — unknown address, no\n code requested, expired code, wrong code — answers `\"INVALID_RESET_CODE\"`, and\n an exhausted code answers `\"RESET_CODE_ATTEMPTS_EXCEEDED\"`. Reporting the\n remaining count both confirmed the account existed and told an attacker how\n much room was left. Don't parse a number out of `error`.\n- **Email credentials are only saved on success.** `loginWithEmail` and\n `confirmEmailRegistration` persist email+password to local storage (scoped by\n `titleID`) only after `fetchAndApplyClientState` succeeds — a failed login\n doesn't overwrite previously saved credentials, and a fresh install has\n nothing saved until the first successful email login. `registerWithEmail` on\n its own saves nothing: at that point the account does not exist yet.\n- **`refreshSession()` has a re-entrancy guard.** If it's already running\n (e.g. two requests 401 at nearly the same time), a second call returns `null`\n immediately instead of recursing — this exists to avoid deadlocking the\n retry, so don't assume a `null` result means the session is actually dead.\n- **`logout()` forgets the saved login method too (changed in core 0.1.3).** It\n nulls the auth context, wipes the cached user state and title-config bundle,\n clears `lastAuthType` and any saved email/password, and emits\n `auth:loggedOut` — so a later `autoLogin()` has nothing to resume. Before\n 0.1.3 storage survived a logout and the next launch silently signed the\n player back into the account they had just left. If you want a sign-out the\n player is remembered through, don't call `logout()`.\n- **Never reload the page after `logout()`** (or after deleting the account). The host shell\n (`AuthGate`) returns to the sign-in screen by itself on `auth:loggedOut`. A `location.reload()`\n restarts the whole app for nothing and, in the AI Coder's live preview, left a dark empty frame:\n the preview's files arrive by message and do not survive a reload.\n- **`refreshSession()` and `logout()` don't return `OperationResult`.**\n `refreshSession()` resolves to `string | null` (the fresh session ticket, or\n `null` on failure/no saved method); `logout()` is synchronous `void`. Don't\n branch on `.ok` for these two.\n- **`GameLoop` is excluded from the login `ClientState`.** Board/stage state\n and its definitions are fetched separately per-stage by the GameLoop\n feature, not bundled into every login response.\n- **Guard against double-submit.** Each call fires a real request; a\n double-tapped \"Log in\" button can fire twice. Firing the same auth endpoint\n again inside the SDK's own throttle window (default 600 ms) comes back\n `reason: \"throttled\"` rather than duplicating the call, but disable the\n control while a call is in flight rather than relying on that — and note the\n server has its own, stricter per-account lock on top (see above).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — request/response wire\nshapes, the exact backend validation and error-code rules for each login/reset\npath, UserID derivation, and the default-username generator. Read it when you\nneed to reason about _why_ a specific `error` string came back, or when\nbuilding a password-reset or registration UI that needs to match the backend's\ntiming/attempt rules exactly.\n",
|
|
4
|
+
"content": "---\nname: authentication\ndescription: >-\n Log players into a game on the iDosGames TypeScript SDK (@idosgames/core)\n via client.auth (AuthenticationService): guest/device-id login, two-step\n email registration with a confirmation code, email login, Google/Telegram\n login, SSO-code login from idosgames.com, wallet login, password reset, auto\n login on relaunch, session refresh, logout, and client-side email/password\n validation. Use this whenever the user is working in the iDosGames TS SDK or\n its game templates (board-game, idle-rpg) and asks about logging a player\n in, sessions, registration, email confirmation codes, resending a code,\n guest accounts, device-id login, Telegram login, Google login, SSO login,\n wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise\n touches client.auth, AuthenticationService, or AuthContext — even if they\n don't name the module explicitly.\n---\n\n# Authentication (iDosGames TS SDK)\n\nThe Authentication module is how a player gets a session. It's the one module\nevery other feature depends on: nothing else on `client` works until a login\nmethod has succeeded. Unlike feature modules, most of its methods don't return\na narrow per-feature payload — they return the **entire post-login bootstrap\nstate** (`ClientState`: title config + the player's full `User` state),\nbecause a successful login is also \"give me everything the client needs to\nrender.\"\n\nThis skill is for **using** production login flows, not for porting or\nextending the service. If a login is rejected, that's the backend enforcing a\nrule (bad credentials, duplicate email, banned account) — surface the error,\ndon't try to reproduce the check client-side.\n\n## Remember me\n\n`setRememberSession(remember)` decides whether a successful login is written to storage. Call it\n**before** a `login*` method — it is read when that login completes, and it applies to whichever\nprovider runs next.\n\n```ts\nclient.auth.setRememberSession(rememberCheckbox); // default: true\nconst result = await client.auth.loginWithDeviceID();\n```\n\n- **On** (the default, and how every release before core 0.1.3 behaved) — the session is persisted,\n so `autoLogin()` signs the player back in on the next launch.\n- **Off** — nothing is persisted and any previously remembered session is dropped, so the next\n launch opens on the login screen. The current session is NOT weakened: the credentials stay in\n memory for this tab, so the transport's automatic 401 re-login still works.\n\n`logout()` ends the session and clears the persisted one, so a reload does not walk back into the\naccount the player just left. (Before core 0.1.3 it left storage untouched and a reload silently\nsigned them back in.)\n\nSessions are extended by the **refresh token** (`Authentication/RefreshSession`, core ≥ 0.18): every\nlogin returns one, it is kept in memory and — when remembering — in storage, and rotated on every\nrefresh. A 401 and a cold-start `autoLogin()` use it first, so ANY method comes back, wallet and\nGoogle included; replaying the login method is only the fallback for a missing/expired token. On a\ntitle whose login mode needs a token balance the server re-checks it on that refresh; a balance\nthat dropped closes the game (`client.auth.playAccess.Granted === false`), it does not end the session.\n\n## Mental model\n\n- **Guest login** (`loginWithDeviceID`) — anonymous account keyed off a\n per-device ID the platform adapter generates/stores. Zero-friction first\n launch; no credentials to lose, but also nothing to recover if the device ID\n is gone (reinstall, new device).\n- **Linked login methods** — `loginWithEmail` / `registerWithEmail`,\n `loginWithGoogle`, `loginWithTelegram`, `loginWithSsoCode`. These tie\n the account to a real identity so the player can resume it elsewhere.\n- **`autoLogin()`** picks up where the player left off. It first extends the\n remembered session by its **refresh token** (works for every method). Only\n without a usable token does it fall back to replaying — and that fallback\n covers just the methods that don't need a fresh externally-issued token: it replays `loginWithEmail`\n if the last successful login was Email (saved password), `loginWithDeviceID`\n if it was Device/None, and `loginWithTelegram` if it was Telegram (its\n `initData` is re-read live from the Telegram WebApp bridge each time, not\n stored). For Google/Facebook/GooglePlay/platform-token, there's no stored\n credential to replay — `autoLogin()` returns `reason: \"client\"` instead of\n guessing, because DeviceID and email/platform logins hash to **different\n backend UserIDs** (see Gotchas), so silently falling back to a guest login\n would strand the player on an unrelated empty account. What `autoLogin()`\n has to replay is controlled by **\"remember me\"** — see below. Call this on app\n start instead of hand-rolling \"which method did they use last,\" but be ready\n to handle its failure by re-running the platform's sign-in flow.\n- **Session refresh is automatic.** The HTTP transport calls\n `refreshSession()` itself when a request comes back 401 — it calls the\n server's `RefreshSession` with the refresh token (falling back to replaying\n the login method only without a usable token) and retries once. You almost never call\n `refreshSession()` directly; it's documented here for completeness, not as a\n method you wire up in app code.\n- **Static validators** (`AuthenticationService.isValidEmail`,\n `AuthenticationService.isValidPasswordLength`) are plain synchronous\n functions, not calls to the backend — use them to validate a form before\n spending a network round-trip on a login/register call that will fail\n server-side anyway. They mirror, but do not replace, the server's own\n checks — the server always re-validates.\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any other auth.* login method\n\nclient.auth.isLoggedIn; // true once a login call has succeeded\n```\n\nEvery other module's methods require this to have succeeded first — without a\nsession they return `{ ok: false, reason: \"unauthorized\" }` rather than\nthrowing. There is one `client` per player; don't share it across sessions.\nAuthentication requests carry no Bearer ticket at all — login/register/reset\nare the one family of calls that work before you have a session.\n\n## Methods\n\nAll login/register/reset methods return `Promise<OperationResult<T>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args, e.g. missing Telegram initData or empty platform token),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\nthrottle window), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe reason code the backend returned, e.g. `\"INCORRECT_EMAIL_OR_PASSWORD\"`,\n`\"INVALID_VERIFICATION_CODE\"` — see Gotchas for the verbatim set).\n\n| Method | Purpose | `data` on success |\n| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |\n| `loginWithDeviceID()` | Anonymous/guest login keyed off the device ID. | `ClientState` |\n| `loginWithTelegram()` | Login using the Telegram Mini App's init data. | `ClientState` |\n| `loginWithEmail(email, password)` | Login with email + password. | `ClientState` |\n| `registerWithEmail(email, password)` | **Start** a registration. Only mails a code — never returns a session, see below. | `EmailRegistrationOutcome` |\n| `confirmEmailRegistration(email, code)` | Finish the registration with the code from the email, creating the player. | `ClientState` |\n| `resendVerificationCode(email)` | Mail a fresh code. Resolves successfully whatever happened — except when there is no usable sender at all (`EMAIL_SENDER_NOT_CONFIGURED`). | `void` |\n| `requestWalletChallenge(networkID, address)` | Ask for the message the player's wallet must sign. Step one of wallet login. | `WalletChallengeResponse` |\n| `loginWithWallet(networkID, address, signature)` | Exchange the signed challenge for a title session. | `ClientState` |\n| `loginWithGoogle(googleIDToken)` | Login with a Google ID token. | `ClientState` |\n| `loginWithSsoCode(code)` | Exchange a one-time code from idosgames.com for a title session. **Web only** — see below. | `ClientState` |\n| `forgotPassword(email)` | Trigger a password-reset email (a 6-digit code). | `SuccessResponse` |\n| `resetPassword(email, resetToken, password)` | Complete a password reset using the emailed code. | `SuccessResponse` |\n| `autoLogin()` | Replay the last-used auth method (Email/Device/None/Telegram only — fails with `reason: \"client\"` for Google/Facebook/GooglePlay/platform-token). | `ClientState` |\n| `refreshSession()` | Re-authenticate and return a fresh session ticket; used internally by the transport on 401. | `string \\| null` (not an `OperationResult`) |\n| `logout()` | Clear the session, forget the saved login method, and reset the local cache. Synchronous, no network call. | `void` |\n| `setRememberSession(remember)` | \"Remember me\" — whether the NEXT login is persisted for `autoLogin()`. Default `true`. Call before a `login*` method. | `void` |\n| `AuthenticationService.isValidEmail(email)` | Static, synchronous client-side format check. | `boolean` |\n| `AuthenticationService.isValidPasswordLength(pw)` | Static, synchronous length check (8–100 chars). | `boolean` |\n\n⚠ **Each title has its OWN password.** The same address in two games means two independent\npasswords: resetting in one game does not change the login for the other. The reason is that the\nreset e-mail goes out through the PUBLISHER's SMTP — with one shared password, one publisher's mail\nserver would control access to every other publisher's game. So don't tell the player \"the same\npassword as in your other game\", and don't reuse stored credentials across titles.\nThe IDENTITY is still shared: Google, Apple and the idosgames.com sign-in recognise the same person\nin every game. Identity answers \"who is this\", not \"what proves they may sign in\".\n\n⚠ **Signing in through idosgames.com from a NATIVE build is not in this SDK.** `beginSsoRedirect` /\n`loginWithSsoCode` are the web half: the browser comes back to the same page with the code in the\nURL fragment. In a packaged Android/iOS build the browser is a separate program that tells the app\nnothing, so that flow uses a request the game polls for instead (`BeginNativeSsoRequest` /\n`PollNativeSsoRequest`), and it is implemented in the Unity SDK. Don't reach for `loginWithSsoCode`\nfrom a native wrapper — it has no way to receive the code.\n\n⚠ **That native flow now has a SHORT CODE, and a native client should use it.** `BeginNativeSsoRequest`\naccepts `UseUserCode: true` and then answers with `UserCode` (`WDJB-MJHT`) plus `VerificationUri`.\nThe game displays the code; the player opens the plain URL and types it there. The older shape — the\ngame opening `…/sso?title=X&request=<handle>` and the page confirming on its own — still works for\nalready-shipped builds, but **a request created with a code refuses that path**, so a build must do\none or the other, never a mix.\n\nThe reason is not convenience. In the older shape the handle travels IN THE ADDRESS, so the link can\nbe sent to somebody else: the attacker creates the request on their own device, the victim opens the\nlink and confirms, and the attacker's game gets a session as the victim. PKCE does not help (the\nverifier belongs to whoever created the request). With a code there is nothing in the URL to forward\n— the request is named only by what is on the device's own screen. `GetAuthOptions` reports\n`NativeSso.UserCodeSupported` and `NativeSso.PollIntervalSeconds`; ask before assuming, and follow\nthe interval or the poll quota will refuse you.\n\n⚠ **The code is shown, the secret is not.** Players read codes off screens, over shoulders and out\nof screenshots — so the code alone must never buy a session. Polling keeps requiring the request\nsecret, which never leaves the game.\n\nNon-Promise instance getters (read directly, no `await`):\n\n| Getter | Returns |\n| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `client.auth.context` | `AuthContext \\| null` — `{ userID, clientSessionTicket, clientSessionTicketExpiration, platformUserID?, platformAuthToken?, platformAuthTokenExpiration? }`, or `null` if never logged in. |\n| `client.auth.isLoggedIn` | `true` once `context` has a non-empty `userID` and `clientSessionTicket`. |\n| `client.auth.lastAuthType` | The persisted `AuthType` (`\"None\" \\| \"Device\" \\| \"Email\" \\| \"iDosGames\" \\| \"Facebook\" \\| \"Google\" \\| \"GooglePlay\" \\| \"Telegram\"`) from the previous successful login, read from local storage. |\n\nOn a successful login/register call, the SDK mirrors the full `ClientState`\ninto `client.data` (title config + user state) — same cache every other\nmodule reads from — and emits events. `email`/`password` are persisted to\nlocal storage **only** after a successful `loginWithEmail` or\n`confirmEmailRegistration` call, so `autoLogin()` has something to replay.\n⚠ Note it is the CONFIRMATION, not `registerWithEmail`, that persists them:\nregistration alone creates no account, so there is nothing to replay yet (the\npassword is carried over from the first step internally). A leftover\nusername you pass in is only a fallback — Google logins use the Google\nprofile name, platform-token logins use the platform profile name, and if\nnone is available anywhere the backend assigns a generated one (see Gotchas).\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn.\n\n- `auth:requestSent` → `void` — fired at the start of every login/reset call (before the network round-trip). One exception: `loginWithSsoCode` validates its argument first, so an empty-code `reason: \"client\"` failure doesn't fire it.\n- `auth:loggedIn` → `void` — fired after a login/register call succeeds and `ClientState` has been applied to the cache.\n- `auth:loggedOut` → `void` — fired synchronously by `logout()`.\n- `auth:unauthorized` → `void` — transport-level, fired by the HTTP layer only when a 401 could **not** be transparently recovered (the automatic refresh was unavailable, already attempted, or failed). A 401 that the refresh-and-retry rescues never emits it. Not emitted by `AuthenticationService` itself.\n- `user:clientStateReceived` → `ClientState` — fired whenever a full client state is applied (login, or any other flow that re-fetches it).\n- `user:stateUpdated` → `void` — fired whenever the cached `User` half of `ClientState` is replaced, which includes every successful login.\n- `user:anyUpdated` → `void` — coarsest \"something in the user cache changed\" signal; also fires on login.\n\n```ts\nconst off = client.on(\"auth:loggedIn\", () => {\n console.log(\"logged in as\", client.auth.context?.userID);\n});\n// later: off();\n```\n\n## Recipes\n\n### Guest login on first launch\n\nIn the base template the \"Play as guest\" button is OFF by default; a project turns it on with\n`auth: { guest: true }` in `src/app.config.ts`. The Google button shows only when the title has its\nOWN Google client (`getAuthOptions()` → `Google.Enabled` and not `Google.PlatformFallback`).\n\n```ts\nconst result = await client.auth.loginWithDeviceID();\nif (!result.ok) return showError(result.error ?? result.reason);\n// client.data now has the full ClientState; render the game.\n```\n\n### Try auto-login first, fall back appropriately\n\n```ts\nasync function bootstrap() {\n const result = await client.auth.autoLogin();\n if (result.ok) return; // resumed the last session\n if (result.reason === \"client\" && client.auth.lastAuthType !== \"None\") {\n // Last session used Google/Facebook/GooglePlay/a platform token — autoLogin\n // can't replay that silently. Re-run that platform's sign-in flow and call\n // the matching login method (e.g. loginWithGoogle) with a fresh token.\n return reauthenticateViaPlatformSDK(client.auth.lastAuthType);\n }\n showError(result.error ?? result.reason);\n}\n```\n\n`autoLogin()` safely self-replays Email, Device/None, and Telegram — call it\nonce on app start for those. For anything else it deliberately does **not**\nfall back to a guest login (see Gotchas) — treat its `reason: \"client\"`\nfailure as \"go get a fresh token,\" not as a generic error to toast.\n\n### Register a new account with email\n\n```ts\nif (!AuthenticationService.isValidEmail(email)) {\n return showFieldError(\"email\", \"Enter a valid email address.\");\n}\nif (!AuthenticationService.isValidPasswordLength(password)) {\n return showFieldError(\"password\", \"Password must be 8–100 characters.\");\n}\n\nconst started = await client.auth.registerWithEmail(email, password);\nif (!started.ok) return showError(started.error ?? started.reason);\n\n// No account yet — a code was mailed. Show a code screen; the session arrives from confirm.\nconst done = await client.auth.confirmEmailRegistration(email, codeFromPlayer);\nif (!done.ok) return showError(done.error ?? done.reason);\n```\n\n⚠ **`registerWithEmail` never creates an account, and success does not mean the address was\nfree.**\n\nRegistration always stores the attempt and mails a 6-digit code; the player is created by\n`confirmEmailRegistration`. Building a screen that assumes an immediate session will leave players\nstaring at a spinner that never resolves.\n\nConfirmation is **platform behaviour, not a title setting** — there is no flag to switch it off,\nand no `verificationRequired` branch to write. Only the numbers are configurable (code lifetime,\nattempt cap, resend cooldown).\n\nThe answer is also deliberately **identical for a free and for a taken address** — there is no\n`EMAIL_ALREADY_EXISTS` on this path any more. Telling them apart would turn the registration form\ninto a way of checking whether a given person has an account here, which is exactly what the\npassword-recovery form goes out of its way to avoid. The owner of a taken address gets a \"you\nalready have an account\" e-mail instead, without a code.\n\nOne failure on this path **is** reported: `EMAIL_SENDER_NOT_CONFIGURED`, when the server has no\nusable mail sender at all. It says nothing about the address — the same answer comes back for a\nfree one, a taken one and a typo — and it exists because confirmation cannot be switched off: with\nno sender, e-mail registration does not work at all, and answering \"code sent\" would hide a broken\ndeployment behind a normal-looking screen. Show it as a server-side problem, not as the player's\nmistake, and do not offer a \"resend\" — the resend refuses the same way. A letter that merely fails\nto leave is a different case and still answers success.\n\nEvery way of getting the code wrong — no such registration, expired, mismatched — answers with the\nsame `INVALID_VERIFICATION_CODE`, for the same reason. Do not branch on it.\n\n`resendVerificationCode` mails a **new** code: only its hash is stored, so the previous one cannot\nbe re-sent and stops working. Rate-limit the button in the UI (the server's cooldown is reported as\n`resendCooldownSeconds`) — the server answers successfully even when it sent nothing.\n\nEmail is matched case-insensitively — the backend trims and lowercases it\nbefore every lookup and write, so `Player@Mail.com` and `player@mail.com`\ncollide on the same account.\n\n### Upgrade a guest account to an email account\n\nThe SDK has no dedicated \"link email to this guest\" call — `registerWithEmail`\nalways mints its **own** account (a fresh UserID derived from the email, via a\ndifferent hash than the device-id account uses). To move a guest forward\nwithout losing their progress, do it through your own game-side flow (e.g. a\ncloud script that copies/merges state) rather than assuming registration\ncarries the guest's inventory along; don't build a UI that implies it will.\n\n### Forgot / reset password\n\n```ts\nconst sent = await client.auth.forgotPassword(email);\nif (!sent.ok) return showError(sent.error ?? sent.reason);\n// If an account exists for this email, a 6-digit reset code was emailed,\n// valid for 5 minutes. The response looks identical whether or not the\n// email is registered (see Gotchas) — don't tell the player \"no such account.\"\n\nconst reset = await client.auth.resetPassword(email, resetToken, newPassword);\nif (!reset.ok) return showError(reset.error ?? reset.reason);\n// password changed; call loginWithEmail with the new password next\n```\n\n### Handle a rejected login\n\n```ts\nconst result = await client.auth.loginWithEmail(email, password);\nif (!result.ok) {\n switch (result.reason) {\n case \"connection\":\n return offerRetry();\n case \"throttled\":\n return; // ignore — same call already in flight/just ran\n case \"client\":\n case \"server\":\n default:\n return showError(result.error ?? \"Login failed.\");\n }\n}\n```\n\n### Log out\n\n```ts\nclient.auth.logout(); // synchronous — clears context + resets client.data cache\n// route to login screen; no network call is made\n```\n\n## Gotchas\n\n- **A title's login mode can refuse methods** (`cfg.Blockchain.PlayAccess`). `WalletOnly` accepts\n only wallet login (others fail with `PLAY_ACCESS_WALLET_LOGIN_ONLY`); `AnyWithLinkedWallet` refuses\n guests (`PLAY_ACCESS_GUEST_DISABLED`) and lets other logins in with `client.auth.playAccess.Granted\n=== false` until a wallet is linked and holds enough tokens. Read the mode BEFORE login with\n `client.auth.getAuthOptions()` (`PlayAccessMode`) to hide dead buttons. The host template does all\n of this already — see the `blockchain-system` skill, \"Login mode and play access\".\n- **The wallet button is not offered on an open game.** `getAuthOptions().ShowWalletLogin` is true\n only when wallet login is on AND the mode is token-gated or the publisher set\n `Blockchain.WalletLogin.ShowOnLoginScreen` (`save_blockchain`). `DefaultLoginScreen` follows it — to\n show the button on an open game, set that flag on the title; do not hard-code the button.\n- **`autoLogin()` deliberately refuses to guess for federated/platform\n logins.** The backend derives DeviceID UserIDs from\n `SHA256(platform + device + deviceID)` and email/platform-linked UserIDs\n from `SHA256(email + titleID)` — two different hashes with no server-side\n link between them. Falling back to `loginWithDeviceID()` for a\n Google/Facebook/GooglePlay/platform-token session would silently resolve to\n a different, empty account, not \"safely resume as a guest.\" If\n `lastAuthType` is one of those, `autoLogin()` returns\n `{ ok: false, reason: \"client\" }` instead; the app must obtain a fresh token\n from that platform's SDK and call the matching `login*` method itself.\n- **The exact `error` strings on `reason: \"server\"` are backend-defined\n codes/messages**, not prose meant for direct display — surface them through\n your own copy/localization layer rather than showing them raw. Verified\n values from the backend for the flows above: `\"INVALID_INPUT_DATA\"`\n (missing/blank required field) and `\"INCORRECT_EMAIL_OR_PASSWORD\"` (bad\n credentials **or** unknown email — the backend intentionally doesn't\n distinguish the two, so don't tell the player \"no such account\") and\n `\"INVALID_VERIFICATION_CODE\"` (every way of getting the confirmation code\n wrong, collapsed into one) are shared\n `MessageCode` enum values also used elsewhere in the backend;\n `\"EMAIL_ALREADY_EXISTS\"` is **no longer returned by registration** — the form\n answers identically for a free and for a taken address, on purpose;\n `\"RATE_LIMIT_EXCEEDED\"` / `\"OPERATION_IN_PROGRESS\"` (per-account login lock\n — see below) are the same enum family. Google/platform-token logins instead\n return ad-hoc string literals specific to that flow, e.g. `\"BANNED_GLOBAL\"`\n (platform account is banned), `\"GOOGLE_ACCOUNT_CONFLICT\"` (email already\n linked to a _different_ Google account), `\"INVALID_GOOGLE_TOKEN\"`\n (bad/expired Google ID token) — see\n [references/data-model.md](references/data-model.md) for the full list.\n Telegram login instead surfaces plain sentences like `\"Invalid Telegram\ndata\"` / an internal `\"Telegram auth_date is stale\"` condition (initData\n older than 24h or timestamped implausibly in the future) — treat any\n non-uppercase-code string as an opaque message, not something to\n pattern-match on.\n- **Per-account login is rate-limited server-side, independent of the SDK's\n own 600 ms throttle.** Repeated `loginWithEmail` attempts for the same\n account inside a ~1-second window come back `\"RATE_LIMIT_EXCEEDED\"`; a login\n already being processed for that account comes back\n `\"OPERATION_IN_PROGRESS\"`. This is a brute-force guard, not a bug — don't\n retry-loop past it.\n- **Telegram login needs the platform adapter's init data.** `loginWithTelegram()`\n calls `platform.getTelegramInitDataRaw()` first; if that returns falsy (not\n running inside Telegram, or the adapter doesn't support it), the call fails\n client-side with `reason: \"client\"` before any network request — no point\n retrying without fixing the environment. Server-side, the init data is\n rejected if its `auth_date` is missing, more than 24 hours old, or more than\n 5 minutes in the future — a Telegram Mini App that's been idle a long time\n may need a fresh WebApp launch, not just a retry. On success it's recorded\n under its own `AuthType.Telegram` (not `Device`) precisely so `autoLogin()`\n replays `loginWithTelegram()` again rather than a device-id login.\n- **`loginWithPlatformToken` no longer exists.** The site used to hand games the\n platform auth token — a 24-hour string that also authorizes the whole\n management API. It was replaced by the one-time SSO code\n (`loginWithSsoCode`), and the endpoint has since been removed as unreachable.\n A session opened by that code is still recorded under `AuthType.iDosGames`,\n which is what `lastAuthType` reports afterward.\n- **A username you pass in is a last resort, not a guarantee.** For Google and\n platform-token logins, the backend prefers the identity provider's own\n profile name over any client-supplied username on every login (not just the\n first) — so a locally cached display name can be silently overwritten by the\n linked account's name. If no name is available from any source at\n registration time, the backend assigns a generated one\n (`\"BraveTiger482\"`-style: adjective + noun + 2–4 digit number) rather than\n leaving it blank.\n- **`forgotPassword` never reveals whether an email is registered.** Unknown\n emails and known emails both return `{ ok: true }` with no email actually\n sent for the unknown case — this is a deliberate anti-enumeration measure,\n not a bug to work around. The emailed reset code is a 6-digit number valid\n for 5 minutes; requesting again inside a 60-second cooldown silently\n no-ops (still returns `ok: true`, no second email). `resetPassword` allows\n at most 5 wrong-code attempts before the code is invalidated server-side and\n a fresh `forgotPassword` call is required.\n ⚠ **The remaining-attempt count is NOT in the response any more**, and neither\n is \"no such account\": every outcome of `resetPassword` — unknown address, no\n code requested, expired code, wrong code — answers `\"INVALID_RESET_CODE\"`, and\n an exhausted code answers `\"RESET_CODE_ATTEMPTS_EXCEEDED\"`. Reporting the\n remaining count both confirmed the account existed and told an attacker how\n much room was left. Don't parse a number out of `error`.\n- **Email credentials are only saved on success.** `loginWithEmail` and\n `confirmEmailRegistration` persist email+password to local storage (scoped by\n `titleID`) only after `fetchAndApplyClientState` succeeds — a failed login\n doesn't overwrite previously saved credentials, and a fresh install has\n nothing saved until the first successful email login. `registerWithEmail` on\n its own saves nothing: at that point the account does not exist yet.\n- **`refreshSession()` has a re-entrancy guard.** If it's already running\n (e.g. two requests 401 at nearly the same time), a second call returns `null`\n immediately instead of recursing — this exists to avoid deadlocking the\n retry, so don't assume a `null` result means the session is actually dead.\n- **`logout()` forgets the saved login method too (changed in core 0.1.3).** It\n nulls the auth context, wipes the cached user state and title-config bundle,\n clears `lastAuthType` and any saved email/password, and emits\n `auth:loggedOut` — so a later `autoLogin()` has nothing to resume. Before\n 0.1.3 storage survived a logout and the next launch silently signed the\n player back into the account they had just left. If you want a sign-out the\n player is remembered through, don't call `logout()`.\n- **Never reload the page after `logout()`** (or after deleting the account). The host shell\n (`AuthGate`) returns to the sign-in screen by itself on `auth:loggedOut`. A `location.reload()`\n restarts the whole app for nothing and, in the AI Coder's live preview, left a dark empty frame:\n the preview's files arrive by message and do not survive a reload.\n- **`refreshSession()` and `logout()` don't return `OperationResult`.**\n `refreshSession()` resolves to `string | null` (the fresh session ticket, or\n `null` on failure/no saved method); `logout()` is synchronous `void`. Don't\n branch on `.ok` for these two.\n- **`GameLoop` is excluded from the login `ClientState`.** Board/stage state\n and its definitions are fetched separately per-stage by the GameLoop\n feature, not bundled into every login response.\n- **Guard against double-submit.** Each call fires a real request; a\n double-tapped \"Log in\" button can fire twice. Firing the same auth endpoint\n again inside the SDK's own throttle window (default 600 ms) comes back\n `reason: \"throttled\"` rather than duplicating the call, but disable the\n control while a call is in flight rather than relying on that — and note the\n server has its own, stricter per-account lock on top (see above).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — request/response wire\nshapes, the exact backend validation and error-code rules for each login/reset\npath, UserID derivation, and the default-username generator. Read it when you\nneed to reason about _why_ a specific `error` string came back, or when\nbuilding a password-reset or registration UI that needs to match the backend's\ntiming/attempt rules exactly.\n",
|
|
5
5
|
"references": [
|
|
6
6
|
{
|
|
7
7
|
"path": "data-model.md",
|