{"id":50150,"plugin_id":"plugin_asdk_app_6abf2061a1248191a70d61197a20542a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:42:16.469Z","digest":"743eea2ccb2f8597915747af18327e08118c14b02b6865fd0ba5bc66d69c5f03","against":null,"payload":{"description":"Implements Hive Axyl Custom login in Unity using an app-server one-time grant key through PKCE, token exchange and session installation. Use for grant-key login and fresh-key recovery; the client does not obtain server credentials.","included_files":[],"name":"axyl-custom-login","skill_md_contents":"---\nname: axyl-custom-login\ndescription: \"Implements Hive Axyl Custom login in Unity using an app-server one-time grant key through PKCE, token exchange and session installation. Use for grant-key login and fresh-key recovery; the client does not obtain server credentials.\"\nlicense: Apache-2.0\nmetadata:\n  author: Com2uS Platform Corporation\n  sdk-version: 1.0.0\n  engine: Unity 6000.0+\n---\n\n# Custom login with an app-server grant\n\nSigns in a player the game authenticated itself. The game's **server** calls\n`/auth/v1/grant` with `authType=CUSTOM_LOGIN`, and the client submits only the\nreturned one-time key. The Installed SDK, SDK results, Axyl session and Login\nmetadata sections below apply to every step.\n\n**Inputs:** a nonblank `grantKey`, Axyl `clientId` and `deviceKey`. The client never\nholds the game's account password or any server credential.\n**App owns:** the game's own login, its server integration, UI and result types.\n\n## Flow\n\n1. A blank key stops before any request.\n2. New PKCE pair, then `auth.LoginCustomProviderAsync(new CustomLoginRequest {\n   GrantKey, ClientId, DeviceKey, CodeChallenge,\n   CodeChallengeMethod = CodeChallengeMethod.S256 }, ctx)`.\n3. On `AuthLoginCustomProviderResult.Success`, exchange `Data.AuthorizationCode`\n   with the same verifier and install the session. Grant acceptance alone is not\n   a login.\n\n## Rejected or uncertain keys\n\n- `InvalidGrantKey`: ask the app server for a fresh key. Keys are single-use and\n  live about 60 s; the server contract decides.\n- Other typed or unknown outcomes are not fixed by a new key; report them. No\n  outcome proves that the sent key is still unspent.\n- After a timeout, failure or cancellation, never resend the same key or code; a\n  new attempt needs a new key and a new PKCE pair.\n- A failed attempt leaves the previous session and recovery data untouched.\n\n## Verify\n\n- [ ] A blank key makes no auth call; a valid attempt sends only the app-server key.\n- [ ] The returned code is exchanged with the same attempt's verifier, followed by\n  one `SetSession`.\n- [ ] `InvalidGrantKey` requests a fresh key; other outcomes keep their meaning;\n  nothing replays a key or a code.\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":{}}