npm package: @flow-industries/auth
Description: Passkey-first authentication for Flow applications. One passkey bound to `id.flow.industries`, usable across all Flow apps, with optional Tempo chain support.
Latest version: 0.1.0

# Flow Auth

Passkey-first authentication for Flow applications. One passkey bound to `id.flow.industries`, usable across all Flow apps, with optional Tempo chain support.

## Integration

There are two ways to integrate Flow Auth into your app:

### Option 1: Wagmi connector

Best for apps that use wagmi/viem and want standard React hooks (`useAccount`, `useConnect`, `useSendTransaction`).

**Install dependencies:**

```bash
bun add wagmi viem @tanstack/react-query
```

**Set up the config:**

```ts
// config.ts
import { createConfig, http, createStorage } from "wagmi"
import { tempo } from "viem/chains"
import { flow } from "@flow/auth/client"

export const config = createConfig({
  chains: [tempo],
  connectors: [
    flow({
      host: "https://id.flow.industries/dialog",
      rpId: "id.flow.industries",
    }),
  ],
  storage: createStorage({ storage: localStorage }),
  transports: {
    [tempo.id]: http(),
  },
})
```

**Wrap your app:**

```tsx
// main.tsx
import { WagmiProvider } from "wagmi"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { config } from "./config"

const queryClient = new QueryClient()

createRoot(document.getElementById("root")!).render(
  <WagmiProvider config={config}>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </WagmiProvider>
)
```

**Use in components:**

```tsx
import { useAccount, useConnect, useConnectors, useDisconnect } from "wagmi"

function Auth() {
  const { connect, isPending, error } = useConnect()
  const [connector] = useConnectors()
  const account = useAccount()
  const { disconnect } = useDisconnect()

  if (account.isConnected) {
    return (
      <div>
        <p>Connected: {account.address}</p>
        <button onClick={() => disconnect()}>Sign out</button>
      </div>
    )
  }

  return (
    <div>
      {/* Sign up — opens dialog with username + passkey creation */}
      <button onClick={() => connect({
        connector,
        capabilities: { type: "sign-up" },
      } as any)}>
        Sign up
      </button>

      {/* Sign in — opens dialog, passkey prompt immediately */}
      <button onClick={() => connect({
        connector,
        capabilities: { type: "sign-in" },
      } as any)}>
        Sign in
      </button>

      {/* Welcome screen — opens dialog, user chooses */}
      <button onClick={() => connect({ connector })}>
        Sign in with Flow
      </button>

      {error && <p>{error.message}</p>}
    </div>
  )
}
```

After connecting, the user's Tempo address is available via `useAccount()`. The passkey credential is persisted in localStorage — `useAccount()` returns the address on page reload without re-prompting.

### Option 2: Direct dialog host

Best for apps that don't use wagmi, or want full control over the dialog lifecycle.

```ts
import { createDialogHost } from "@flow/auth/client"

const dialog = createDialogHost({
  host: "https://id.flow.industries/dialog",
})
```

**Sign up:**

```ts
const result = await dialog.request("wallet_connect", [
  { capabilities: { createAccount: true } },
])
// result: { user: { id, username }, credential: { id, publicKey } }
```

**Sign in:**

```ts
const result = await dialog.request("wallet_connect", [
  { capabilities: { signIn: true } },
])
// result: { user: { id, username }, credential: { id, publicKey } }
```

**Welcome screen** (user chooses sign up or sign in):

```ts
const result = await dialog.request("wallet_connect", [
  { capabilities: {} },
])
```

**Check session:**

```ts
const res = await fetch("https://id.flow.industries/api/me", {
  credentials: "include",
})
const { session } = await res.json()
// session.user.username, session.user.id, etc.
```

**Close the dialog:**

```ts
dialog.close()   // close the iframe
dialog.destroy() // remove from DOM entirely
```

## How it works

1. Your app opens the Flow Auth dialog (iframe at `id.flow.industries`)
2. The dialog handles passkey creation/authentication + username onboarding
3. A session cookie is set on `id.flow.industries`
4. The credential (id + publicKey) is returned to your app
5. For Tempo chain apps: `Account.fromWebAuthnP256(credential, { rpId })` reconstructs a signing account from the stored passkey

The passkey is bound to `id.flow.industries` via WebAuthn's rpId, so the same passkey works across all Flow apps (`flow.game`, `flow.talk`, etc.) through the shared dialog.

## Server endpoints

| Endpoint | Description |
|---|---|
| `GET /api/config` | Returns `{ rpId, rpName }` |
| `GET /api/me` | Returns current session/user (requires session cookie) |
| `GET /api/account/username/:name` | Check username availability |
| `GET /api/account/account/:address` | Look up username by address |
| `GET /keys/challenge` | Generate WebAuthn challenge |
| `GET /keys/:credentialId` | Get stored public key |
| `POST /keys/:credentialId` | Store public key (with attestation verification) |
| `POST /api/auth/passkey/register` | Create user + passkey + session |
| `POST /api/auth/passkey/challenge` | Generate sign-in challenge |
| `POST /api/auth/passkey/verify` | Verify passkey signature + create session |

## Development

```bash
bun install
bun run db:push    # create/update database tables
bun run dev        # starts server (:3000) + dialog (:5175) + playground (:5176)
```

Playground at `http://localhost:5176` — has both Wagmi and Direct integration demos.