{"id":50179,"plugin_id":"plugin_asdk_app_6abf2061a1248191a70d61197a20542a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:42:17.022Z","digest":"11bf044dadc1a0c1ff2df5ea0c222746a01f40e47371080de7b8453dfa21d83b","against":null,"payload":{"description":"Implements Hive Axyl username/password login and app-requested login-or-signup in Unity, including password transformation, restricted signup fallback, PKCE and session installation.","included_files":[],"name":"axyl-username-login","skill_md_contents":"---\nname: axyl-username-login\ndescription: \"Implements Hive Axyl username/password login and app-requested login-or-signup in Unity, including password transformation, restricted signup fallback, PKCE and session installation.\"\nlicense: Apache-2.0\nmetadata:\n  author: Com2uS Platform Corporation\n  sdk-version: 1.0.0\n  engine: Unity 6000.0+\n---\n\n# Username login or signup\n\nSigns in with a username and password and, only when the app asks for\nlogin-or-signup, creates the account when sign-in fails. The Installed SDK, SDK\nresults, Axyl session and Login metadata sections below apply to every step.\n\n**Inputs:** Axyl `clientId`, `deviceKey`, username, the exact typed password and,\nfor signup, an optional app-server grant key.\n**App owns:** whether signup is offered, UI and result types.\n\n## Flow\n\n1. Password is the lowercase hex SHA-256 of the **exact** UTF-8 input (no trim,\n   case kept). The SDK sends the string as given; never send or log the raw\n   password.\n2. New PKCE pair, then `auth.LoginUsernameAsync(new UsernameLoginRequest {\n   Username, Password = passwordHash, ClientId, DeviceKey, CodeChallenge,\n   CodeChallengeMethod = CodeChallengeMethod.S256 }, ctx)`. On Success, exchange\n   `Data.AuthorizationCode` and install the session.\n3. Sign up only when the app requested it and the result is\n   `AuthLoginUsernameResult.UsernameVerifyFailed` (unknown username **or** wrong\n   password): new PKCE pair, then `auth.CreateUsernameAsync(new\n   UsernameCreateRequest { Username, Password = passwordHash, ClientId,\n   DeviceKey, CodeChallenge, CodeChallengeMethod = CodeChallengeMethod.S256,\n   GrantKey = grantKeyOrNull }, ctx)`. A missing key is `null`; an empty string is\n   sent and refused.\n4. On `AuthCreateUsernameResult.Success`, mark the result as a new account and\n   exchange its code with the **signup** verifier. `UsernameAlreadyExists` means\n   the password was wrong: report incorrect credentials and never sign up again.\n5. Any other result stops at its own stage. Return new-account and block status\n   separately.\n\n## Rules\n\n- The grant key is one-time (60 s) and comes from the app server's\n  `/auth/v1/grant`. It is required when grant-key hardening is on and accepted\n  when it is off (a sent key is always validated and consumed), so sending one is\n  recommended. The recipe comment says to omit it when hardening is off; follow\n  the SDK. `GrantRequiredMissing` or `InvalidGrantKey` needs a fresh key; never\n  invent or replay one.\n- Login-or-signup can create an account for a mistyped new username; expose the\n  new-account result so the UI can show it.\n- A failed or cancelled exchange after signup does not undo the account. The\n  username and password remain the way back, and a retry starts again with a new\n  PKCE pair.\n- Unrelated failures never erase a live session or stored recovery data.\n\n## Verify\n\n- [ ] The hash keeps whitespace, case and UTF-8 and is lowercase SHA-256 hex; no raw\n  password is sent or logged.\n- [ ] Only `UsernameVerifyFailed` enters signup, only when requested, with a fresh\n  PKCE pair; login-only never creates an account.\n- [ ] `UsernameAlreadyExists` and other failures never loop signup; a missing grant\n  key is `null`.\n- [ ] Each code is exchanged with the verifier of the call that returned it; a\n  failure after signup keeps the new-account result without reporting login.\n- [ ] Source review, compilation, test doubles and device/server runs are reported\n  separately.\n\n<!-- axyl-shared: core/installed-sdk.md -->\n\n## Installed SDK\n\n- Read `Packages/manifest.json`, the lock file and each resolved `package.json`.\n  All `com.com2usplatform.hiveaxyl.*` packages must be the same release (UPM does\n  not enforce this); `com.hive.axyl.*` packages are a release before 1.0.0-rc.9.\n- The installed SDK wins for APIs, types and contracts. If its release differs\n  from this skill's `sdk-version`, apply a step only after its fields, result\n  variants, platform support and failure behaviour match; otherwise stop at that\n  step and report what is missing. Never change the installed version.\n- The app owns UI, storage, the device key and orchestration. Sample and recipe\n  classes are not SDK APIs.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/results.md -->\n\n## SDK results\n\nMatch each call's concrete result and read `Success.Data` only after checking the\nfields the next step needs. The markers live in `Hive.Axyl.Contracts.Result`.\n\n| Result | Handling |\n|---|---|\n| `IUserCanceledOutcome` | The user dismissed OS or provider UI: stop this attempt quietly |\n| `IUnknownOutcome` | Keep `Code` and `RawJson`; never treat it as a known refusal or a success |\n| `IUntypedProblem` | Technical failure in `Problem` (`HiveError`): network, timeout or caller `Cancelled` |\n| Other typed variant | Only that call's documented meaning |\n\nPass `new ApiCallContext { Token = ct }` and check cancellation before each\nfollow-up call; a request cancelled in flight may still have taken effect. Never\nbranch on HTTP status, exception text or raw bodies, and never log tokens or\ncredentials.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/auth-session.md -->\n\n## Axyl session\n\n### Setup and inputs\n\n- Initialize once on the main thread with\n  `HiveBootstrap.Initialize(config, b => b.AddAuth().AddToken())`; the default\n  host is `https://core-api.hiveaxyl.com` (use `AddAuth(baseUrl)`/`AddToken(baseUrl)`\n  only for a provisioned host). Call `AuthTokenRefresh.Enable(tokenHost, clientId)`\n  once afterwards with the Token host. Never initialize inside a login.\n- Resolve `IAuthService`, `ITokenService` and `ISessionManager`; if one is missing,\n  stop before any auth call.\n- `clientId` is the Axyl client ID, not a provider's. `deviceKey` is created once\n  per device, persisted and reused: 22–64 printable ASCII (`^[!-~]{22,64}$`, UUID v4\n  recommended); the SDK does not check it and the server rejects anything else.\n- Each attempt uses a fresh PKCE pair (`CodeChallengeMethod.S256`, base64url\n  SHA-256 of a random verifier). Keep the verifier for this attempt's exchange only.\n\n### Authorization code to session\n\n1. `tokens.IssueTokenAsync(new AuthorizationCodeTokenRequest { ClientId,\n   AuthorizationCode, CodeVerifier }, ctx)`.\n2. Continue only on `TokenIssueTokenResult.Success` with a nonempty `AccessToken`\n   and `RefreshToken`. Expiry is Unix seconds now + `ExpiresIn`; if `ExpiresIn` is\n   not positive, use the access token's JWT `exp`; with neither, the response is\n   unusable.\n3. Check cancellation, then call\n   `session.SetSession(accessToken, refreshToken, playerId, expiresAtSec)` once. A\n   failed, unknown, cancelled or superseded attempt never overwrites the current\n   session.\n\n`InvalidGrant`, `InvalidGrantExpired` and `InvalidGrantCodeChallenge` mean this\ncode or verifier is spent: start a new attempt, never replay the code.\n`TemporarilyUnavailable` is the only retryable issue outcome: back off before\nretrying (the interval is the app's choice). `InvalidClient` and `AppNotFound` are\nconfiguration errors.\n\n### Persistence and refresh\n\nSubscribe to `session.OnSessionRefreshed` before `SetSession` and persist each\nsnapshot in order. Core refreshes a live session; never add a refresh loop.\n`OnSessionExpired` means invalidation or an explicit clear: do not delete stored\ncredentials on app quit, transient or unknown failures, or storage `AccessDenied`.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/auth-account.md -->\n\n## Login metadata\n\n- `LoginResponseData` carries `PlayerId`, `CreatedAt`, `AuthorizationCode`,\n  `ProviderList` (`ProviderId`, `ProviderUserId`, `ProviderIndex`) and `IsBlock`.\n  Other responses carry only their own fields. Account creation\n  (`GuestCreateResponseData`, `UsernameCreateResponseData`) returns `PlayerId`,\n  `CreatedAt` and `AuthorizationCode` (guest also `GuestToken`), with no\n  `ProviderList` or `IsBlock`. Token refresh returns tokens only, so earlier\n  metadata stays stale and block status unknown. Never invent missing fields.\n- Return the supplied fields in an app-owned result tied to that player and\n  attempt; the SDK session keeps only tokens, player ID and expiry. The app decides\n  what to persist; authorization codes stay transient.\n- `ProviderList` lists the links at that response; it is not the provider catalog\n  from `GetProviderListAsync`. `Guest` appears only for a pure guest. An empty list\n  or a default value is not proof of no links.\n- `CreatedAt` is not a new-player flag. `IsBlock` is account status for the app to\n  handle, not a transport failure.\n\n<!-- /axyl-shared -->\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}