# Wallet Login

Authenticate browser apps with a wallet-backed session and forward that session to a server.

`PolyesterBrowserClient` runs the full wallet login: request a server-generated Sign-In with Ethereum (SIWE) challenge, sign its exact UTF-8 bytes with an account signer, exchange it for a JWT, and persist the session.

For bots, scripts, and backend services, see [API Keys](https://testnet.polyester.com/docs/sdk/typescript/guides/authentication/api-keys). For the underlying model, see [Authentication model](https://testnet.polyester.com/docs/sdk/typescript/concepts/authentication-model).

## Create an account signer

An account signer identifies the user's Polyester smart account and the owner EOA that authorizes login. From a viem `LocalAccount`, such as a browser wallet, Turnkey wallet, or private key, derive the smart-account address with no RPC calls, then sign login messages directly with the owner:

```ts
import { createPolyesterAccountSigner } from "@polyester/sdk/account-signer";
import { POLYESTER_DEVNET_ENVIRONMENT } from "@polyester/sdk";

const { accountAddress } = createPolyesterAccountSigner({
	environment: POLYESTER_DEVNET_ENVIRONMENT,
	owner: ownerAccount, // a viem LocalAccount
});

const accountSigner = {
	environmentFingerprint: POLYESTER_DEVNET_ENVIRONMENT.fingerprint,
	accountAddress,
	ownerAddress: ownerAccount.address,
	signMessage: (message: string) => ownerAccount.signMessage({ message }),
};
```

`createPolyesterAccountSigner` on its own produces Safe/ERC-6492-wrapped signatures. Those are accepted for subaccount creation, but login requires the owner's raw 65-byte EIP-191 signature, so use it only to derive `accountAddress` in this flow. No account deployment is required.

## Create the client and log in

```ts
import {
	PolyesterBrowserClient,
	POLYESTER_DEVNET_ENVIRONMENT,
	createCookieAuthTokenStorage,
} from "@polyester/sdk";

const client = new PolyesterBrowserClient({
	environment: POLYESTER_DEVNET_ENVIRONMENT,
	accountSigner, // or a factory: () => Promise<AccountSigner | null>
	tokenStorage: createCookieAuthTokenStorage(), // persist across reloads; default is in-memory
});

const result = await client.auth.login({ provider: "metamask" });
console.log("logged in as", result.username, "until", result.expiresAt);
```

The challenge binds the requesting origin, owner EOA, Polyester smart account, Polyester Chain ID, purpose, nonce, issue time, and expiration time. Pass the returned message directly to `personal_sign`. Do not hash it first, reconstruct it, change address casing, normalize whitespace, or append a newline. Login accepts only the owner's raw 65-byte EIP-191 signature. Challenges expire after five minutes and can be used only once.

The SIWE Chain ID is Ethereum mainnet (`1`); the Polyester chain is bound in the message's Resources. Wallets that require the active network to match, such as Phantom, must switch to Ethereum mainnet before signing. `provider` accepts `"metamask"`, `"phantom"`, `"turnkey"`, or `"other"`.

`accountSigner` also accepts a lazy factory when the wallet connects after the client is built. Swap it later with `client.setAccountSigner(signer)`.

## Restore and manage sessions

```ts
// On app start: restore a persisted session. `null` means it is absent, invalid, expired,
// rejected, or superseded by a newer auth operation. A transient request or signer error throws;
// keep the existing session and retry that case.
const restored = await client.auth.restoreSession();

// Refresh with a fresh signature when expiry is close.
if (client.auth.getSessionTimeToExpiry() < 5 * 60_000) {
	await client.auth.refreshSession();
}

// End the session and disconnect private realtime channels.
await client.auth.logout();
```

`client.auth.events` emits `authenticated`, `loggedOut`, `stateChange`, and more. `client.auth.getState()` is synchronous for UI.

Sessions are environment-bound. A token from one environment is ignored when the client is built for another because their fingerprints must match.

Overlapping auth operations are ordered. A newer login, refresh, logout, signer change, or hydration supersedes an earlier login or refresh, which rejects with `AbortError`; a superseded restore returns `null`. This prevents an older response from replacing the newer session.

## Switch accounts and subaccounts

```ts
client.auth.switchAccount(subaccountId);
```

After a switch, service calls that omit `account` scope to the active subaccount. See [Accounts & balances](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances).

## Forward a browser session to a server

On a server handling a signed-in user, build the client from request cookies. The browser session's bearer token rides along:

```ts
import {
	createPolyesterServerClientFromRequest,
	POLYESTER_DEVNET_ENVIRONMENT,
} from "@polyester/sdk";

const client = createPolyesterServerClientFromRequest({
	environment: POLYESTER_DEVNET_ENVIRONMENT,
	request, // the incoming Request
});

const me = await client.verifySession(); // null when not authenticated
```

The server factory accepts a `Request`, a cookie record, or a synchronous `.get(name)` store returning a string or `{ value: string }`. Await framework cookie helpers first; see [Server-side usage](https://testnet.polyester.com/docs/sdk/typescript/guides/server-side) for bearer and display-cookie rules.

See [Server-side usage](https://testnet.polyester.com/docs/sdk/typescript/guides/server-side) for display sessions, verification, and SSR hydration.

## Supply your own JWT

If you already manage tokens, such as through a custom auth proxy or test harness, pass a JWT provider to the core client:

```ts
import { PolyesterClient, POLYESTER_DEVNET_ENVIRONMENT } from "@polyester/sdk";

const client = new PolyesterClient({
	environment: POLYESTER_DEVNET_ENVIRONMENT,
	auth: { kind: "jwt", getToken: () => myTokenStore.current },
});
```

Private realtime channels reuse the client's auth provider. Override per-connection behavior with the `realtime` config option if WebSocket auth needs different headers.
