{"id":50160,"plugin_id":"plugin_asdk_app_6abf2061a1248191a70d61197a20542a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:42:16.703Z","digest":"3b20c605a0a933f91806c0f1abde10b490b59c5bdf479bb02a53af5a3e938314","against":null,"payload":{"description":"Exchanges already-acquired provider credentials for a Hive Axyl session in Unity. Use when the app already has the provider ID, token or ticket and user ID and needs the Axyl login/token/session join. Provider credential acquisition belongs to the selected route skill.","included_files":[],"name":"axyl-provider-login","skill_md_contents":"---\nname: axyl-provider-login\ndescription: \"Exchanges already-acquired provider credentials for a Hive Axyl session in Unity. Use when the app already has the provider ID, token or ticket and user ID and needs the Axyl login/token/session join. Provider credential acquisition belongs to the selected route skill.\"\nlicense: Apache-2.0\nmetadata:\n  author: Com2uS Platform Corporation\n  sdk-version: 1.0.0\n  engine: Unity 6000.0+\n---\n\n# Provider login with acquired credentials\n\nJoins provider credentials the app already holds into an Axyl session.\nThe Installed SDK, SDK results, Axyl session, Login metadata and Provider join\nsections below apply to every step.\n\n**Inputs:** the provider, its token or ticket and its user ID (see the Provider\njoin table), Axyl `clientId` and `deviceKey`.\n**App owns:** acquisition, UI and result types.\n\nIf the credential still has to be acquired, use the installed route skill for\nthat provider (Apple, Credential Manager, Play Games, native Steam or WebAuth).\nWhen none is installed, report the missing input instead of inventing a token.\nThe owner of a native Steam ticket keeps it alive through backend validation and\nreleases it afterwards.\n\n## Verify\n\n- [ ] Credentials map to the right `Provider`, `ProviderToken` and `ProviderUserId`;\n  acquisition and provider-code exchange are not repeated.\n- [ ] A fresh Axyl PKCE pair serves `LoginProviderAsync` and its code exchange,\n  separate from provider OAuth state.\n- [ ] Refusals, unknown results and cancellation stop at their stage without\n  switching provider or creating a guest; block status comes from `IsBlock`.\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\n<!-- axyl-shared: core/provider-join.md -->\n\n## Provider join\n\nTurns an acquired provider credential into an Axyl session. Acquisition, and any\nprovider-code exchange, must have succeeded first; a cancelled, failed or unknown\nacquisition stops before this join.\n\n| Route | `Provider` | `ProviderToken` | `ProviderUserId` |\n|---|---|---|---|\n| Apple native | `SigninApple` | `IdentityToken` | `UserIdentifier` |\n| Google Credential Manager | `Google` | `IdToken` | `UniqueId` |\n| Google/X/Apple browser, Play Games | `Google`, `X`, `SigninApple`, `GooglePlayGames` | `ProviderToken` from `ExchangeProviderTokenAsync` (direct exchange: Google `id_token`, X access token) | its `ProviderUserId` (direct exchange: `sub`, `data.id`) |\n| Steam native | `Steam` | `TicketHex` | the app's SteamID64; confirm server support before omitting it |\n| Steam OpenID | `Steam` | the callback query without the leading `?` or fragment, bytes unchanged | SteamID64 from `openid.claimed_id` |\n\nNever cast another enum to `Provider` by number.\n\n1. New Axyl PKCE pair, separate from any provider OAuth PKCE or state.\n2. `auth.LoginProviderAsync(new ProviderLoginRequest { ProviderId, ProviderUserId,\n   ProviderToken, ClientId, DeviceKey, CodeChallenge,\n   CodeChallengeMethod = CodeChallengeMethod.S256 }, ctx)`.\n3. On `AuthLoginProviderResult.Success`, exchange `Data.AuthorizationCode` with this\n   Axyl verifier and install the session.\n\n`ProviderTokenError`, `ProviderConfigNotFound`, `ProviderClientInfoNotExists`,\n`ProviderRequestFailed` and app, service or IP outcomes keep their own meaning;\nnever switch route or create a guest because of them. Login has no typed\n`ProviderNotSupported`: that code arrives as `IUnknownOutcome`. Show block UI only\nfrom `IsBlock`. A native Steam ticket stays alive until backend validation settles,\nthen its owner releases it on the main thread.\n\n<!-- /axyl-shared -->\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}