> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cskn.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Passkeys

> Passwordless sign-in and signing with WebAuthn, through one API on every surface.

WebViews cannot run WebAuthn themselves, so in the mobile shell the **shell** runs the ceremony and returns the result. In a browser or the desktop shell, the same call uses the page's own `navigator.credentials`. Either way you get standard WebAuthn JSON to send to your server.

The relying party is always `cskn.app`, so a passkey created in the mobile app also works when the user opens the same app in a browser.

```tsx theme={null}
import { api, usePasskey, base64UrlFromBytes } from '@cskn/sdk';

function Passkeys({ user }: { user: { sub: string; username: string } }) {
  const passkey = usePasskey();

  const register = async () => {
    const { available } = await passkey.isAvailable();
    if (!available) return;

    const { challenge } = await api.post<{ challenge: string }>('/my-app/passkeys/challenge');
    const { credential, cancelled, error } = await passkey.create({
      challenge,
      user: { id: base64UrlFromBytes(new TextEncoder().encode(user.sub)), name: user.username },
      authenticatorSelection: { userVerification: 'required' },
    });

    if (cancelled) return;
    if (error) return showError(error);
    await api.post('/my-app/passkeys', credential);
  };

  const signIn = async () => {
    const { challenge } = await api.post<{ challenge: string }>('/my-app/passkeys/challenge');
    const { credential } = await passkey.get({ challenge, userVerification: 'required' });
    if (credential) await api.post('/my-app/passkeys/verify', credential);
  };

  return <button onClick={register}>Create a passkey</button>;
}
```

<Warning>
  The challenge must come from your server, and verification must happen there. A passkey's security depends on both.
</Warning>

## API

| Method | Resolves |
| - | - |
| `isAvailable()` | `{ available, autofillAvailable, transport, rpId }` |
| `create(options)` | `{ credential }`, `{ cancelled: true }` or `{ error, message? }`. Never throws. |
| `get(options)` | Same shape. `credential.response` has `clientDataJSON`, `authenticatorData`, `signature` and `userHandle?`. |

`transport` is `native` (the shell runs the ceremony), `web` (the page's own WebAuthn) or `none`.

### create options

<ParamField path="challenge" type="string" required>Base64url challenge from your server.</ParamField>
<ParamField path="user" type="{ id?, name?, displayName? }">In the mobile shell, the user handle is always the signed-in user's `sub`.</ParamField>
<ParamField path="excludeCredentials" type="PasskeyCredentialDescriptor[]">Credentials the user already has.</ParamField>
<ParamField path="authenticatorSelection" type="object">Standard WebAuthn selection, for example `{ userVerification: 'required' }`.</ParamField>
<ParamField path="pubKeyCredParams / attestation / timeout" type="…">Standard WebAuthn options.</ParamField>

### get options

<ParamField path="challenge" type="string" required>Base64url challenge from your server.</ParamField>
<ParamField path="allowCredentials" type="PasskeyCredentialDescriptor[]">Omit for discoverable sign-in.</ParamField>
<ParamField path="userVerification" type="'discouraged' | 'preferred' | 'required'">Standard WebAuthn option.</ParamField>

## Details

* **Binary fields are base64url.** Convert with `base64UrlFromBytes(bytes)` and `bytesFromBase64Url(text)`.
* **`response.publicKey` from `create` is always DER SubjectPublicKeyInfo**, the same as a browser's `getPublicKey()`. The shell normalizes the platform differences.
* **Error codes:** `not_supported`, `not_configured`, `no_credentials`, `rp_not_allowed`, `invalid_request`, `interrupted`, `failed`.
* **TV has no passkeys** (`transport: 'none'`).

Functions: `isPasskeyAvailable`, `createPasskey`, `getPasskey`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.