{"id":50155,"plugin_id":"plugin_asdk_app_6abf2061a1248191a70d61197a20542a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:42:16.605Z","digest":"98613df98a1cb7117e605cae27c93fb993c1fc77ffe8be4cd94c1efa90b45a89","against":null,"payload":{"description":"Implements first-run or returning guest login in Unity with the Hive Axyl SDK, through token exchange and session installation. Use for guest creation/restoration and retaining guest recovery credentials after failure. Does not implement saved access/refresh-token auto login.","included_files":[],"name":"axyl-guest-login","skill_md_contents":"---\nname: axyl-guest-login\ndescription: \"Implements first-run or returning guest login in Unity with the Hive Axyl SDK, through token exchange and session installation. Use for guest creation/restoration and retaining guest recovery credentials after failure. Does not implement saved access/refresh-token auto login.\"\nlicense: Apache-2.0\nmetadata:\n  author: Com2uS Platform Corporation\n  sdk-version: 1.0.0\n  engine: Unity 6000.0+\n---\n\n# Guest login\n\nCreates a guest on first run or restores the saved one, then installs an Axyl\nsession. The Installed SDK, SDK results, Axyl session and Login metadata sections\nbelow apply to every step.\n\n**Inputs:** Axyl `clientId`, the device's `deviceKey`, the saved\n`(PlayerId, GuestToken)` if any, and an optional app-server grant key.\n**App owns:** credential storage, guest UI, error display and result types.\n\n## Flow\n\n1. Create one PKCE pair for the attempt.\n2. **No saved credential:** `var created = await auth.CreateGuestAsync(new\n   GuestCreateRequest { ClientId, DeviceKey, CodeChallenge, CodeChallengeMethod =\n   CodeChallengeMethod.S256, GrantKey }, ctx)`. On `AuthCreateGuestResult.Success`,\n   persist `(Data.PlayerId, Data.GuestToken)` **before any other check or step**:\n\n   ```csharp\n   if (!(created is AuthCreateGuestResult.Success ok) || string.IsNullOrEmpty(ok.Data.GuestToken))\n       return; // no guest exists yet: report the result\n   await store.SaveGuestAsync(ok.Data.PlayerId, ok.Data.GuestToken); // app-owned storage\n   if (string.IsNullOrEmpty(ok.Data.AuthorizationCode))\n       return; // guest kept: restore it next time\n   ```\n3. **Saved credential:** call `auth.LoginGuestAsync(new GuestLoginRequest {\n   GuestPlayerId, GuestToken, ClientId, DeviceKey, CodeChallenge,\n   CodeChallengeMethod = CodeChallengeMethod.S256 })`.\n4. Exchange `Data.AuthorizationCode` with the step 1 verifier and install the\n   session as the Axyl session section describes. A missing code is unusable.\n5. Return the login metadata and the guest credential to the app.\n\n## Keep the guest recoverable\n\n- Once creation returns a credential, every later exit keeps it: a missing code,\n  a failed or unusable exchange, cancellation or a crash. The next attempt restores\n  it with `LoginGuestAsync`; never create another guest because a later step failed.\n- A failed restore never falls back to creation, never deletes the saved\n  credential and never reports first-run success; return the typed outcome to the\n  app. `InvalidGuestToken` can mean a provider was linked to this guest (the first\n  link invalidates the guest token); the app decides recovery, such as provider\n  login.\n- `GrantKey` is a one-time key (60 s) that the app server gets from\n  `/auth/v1/grant`. It is required when grant-key hardening is on and accepted when\n  it is off (a sent key is always validated and consumed), so sending one is\n  recommended. Never invent or replay a key; `GrantRequiredMissing` or\n  `InvalidGrantKey` needs a fresh one.\n\n## Verify\n\n- [ ] No saved credential selects `CreateGuestAsync`, a saved one `LoginGuestAsync`;\n  a failed restore never creates a guest.\n- [ ] The created `(PlayerId, GuestToken)` is persisted before the code exchange and\n  survives every later failure or cancellation.\n- [ ] The code is exchanged once with the same attempt's verifier, followed by one\n  `SetSession`.\n- [ ] Typed, unknown, technical and cancelled results keep their meaning; a stale\n  attempt never replaces a newer session.\n- [ ] Refreshed session snapshots are persisted in order.\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":{}}