# wata

Latest version **0.4.1** (published 2026-08-31) · MIT license · 0 weekly downloads

## Install

```sh
npm install wata
pnpm add wata
yarn add wata
bun add wata
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.4.1 |
| Published | 2026-08-31 |
| First published | 2025-06-19 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 6 |
| Unpacked size | 2.4 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | jmoxey |

## Links

- npm: https://www.npmjs.com/package/wata
- npm.io page: https://npm.io/package/wata

## Dependencies (6)

- [ox](https://npm.io/package/ox.md) ~0.14.20
- [zod](https://npm.io/package/zod.md) ^4.4.3
- [hono](https://npm.io/package/hono.md) ^4.6.14
- [@noble/hashes](https://npm.io/package/@noble/hashes.md) ^2.2.0
- [@noble/ciphers](https://npm.io/package/@noble/ciphers.md) ^2.2.0
- [@hono/node-server](https://npm.io/package/@hono/node-server.md) ^1.13.7

## Recent versions

- 0.4.1 (latest) — 2026-08-31
- 0.4.0 — 2026-06-19
- 0.3.0 — 2026-06-18
- 0.2.1 — 2026-06-18
- 0.2.0 — 2026-06-17
- 0.1.1 — 2026-06-17
- 0.1.0 — 2026-06-16
- 0.0.4 — 2026-06-11
- 0.0.3 — 2026-06-11
- 0.0.2 — 2026-06-10
- 0.0.1 — 2026-06-08
- 0.0.0 — 2025-06-19

## README

# Wata

## Install

```bash
npm i wata
```

```bash
pnpm i wata
```

```bash
bun i wata
```

## Transports

| Transport         | Description                                                                                                                                                          | Peers             |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `postMessage`     | Same-device browser session over a `Window`, `WindowProxy`, or `MessagePort` (popup, iframe, channel).                                                               | Browser ⇄ Browser |
| `deviceCode`      | OAuth 2.0 Device Authorization Grant (RFC 8628) over HTTP, with PKCE and a bring-your-own approval UI.                                                               | CLI ⇄ Browser     |
| `mobileWebAuth`   | Same-device mobile app to web host flow using browser auth and encrypted app-link callbacks.                                                                         | Mobile ⇄ Browser  |
| `webhookCallback` | Signed HTTP registration + callback flow for consumers that can receive webhooks.                                                                                    | Server ⇄ Server   |
| `relay`           | Remote session over an untrusted HTTPS relay; the web consumer shows a QR/link, the mobile host scans it, exchanging end-to-end-encrypted bodies (SSE or long-poll). | Web ⇄ Mobile      |
| `mobileLink`      | Direct, ongoing same-device session between two mobile apps over OS deep links                                                                                       | Mobile ⇄ Mobile   |

## Usage

### `postMessage`

Same-device browser session over a `Window`, `WindowProxy`, or `MessagePort`. The consumer supplies a `target` (popup, iframe, or channel port); the host defaults to its opener.

[See example →](./examples/postMessage)

#### Consumer

Opens a popup at the host URL and sends a `wallet_connect` request once the handshake completes.

```ts
import { Wata, postMessage } from 'wata'

const wata = Wata.create({
  transports: [postMessage()],
})

const session = wata.start({
  host: 'https://wallet.example',
  target(c) {
    return window.open(c.host, '_blank', 'popup=1')
  },
})

const { result } = await session.send({
  method: 'wallet_connect',
  params: [],
})
```

#### Host

Listens on its opener for incoming requests and responds to `wallet_connect` with a list of addresses.

```ts
import { Wata, postMessage } from 'wata/host'

const wata = Wata.create({
  transports: [postMessage()],
})

const session = wata.start()

session.onRequest(async (c) => {
  if (c.method === 'wallet_connect')
    await c.respond(['0x0000000000000000000000000000000000000001'])
})
```

### `deviceCode`

Cross-device session over HTTP using the OAuth 2.0 Device Authorization Grant (RFC 8628) with PKCE. The consumer surfaces a short `user_code` to the user and polls until the host approves.

[See example →](./examples/deviceCode)

#### Consumer

Requests a device code from the host, prints the verification URL and `user_code` for the user, then polls until approval and dispatches a `wallet_connect` request.

```ts
import { Wata, deviceCode } from 'wata'

const wata = Wata.create({
  transports: [deviceCode()],
})

const session = wata.start({ url: 'https://wallet.example/auth/device' })

session.onPrompt((prompt) => {
  console.log(`Visit ${prompt.verificationUri} and enter ${prompt.userCode}`)
})

const { result } = await session.send({
  method: 'wallet_connect',
  params: [],
})
```

#### Host

Mounts the device-code endpoints under `/auth/device`, renders a minimal approval form, marks the request as approved on submit, and answers `wallet_connect` requests.

```ts
import { createServer } from 'node:http'
import { Store, Wata, deviceCode } from 'wata/host'
import * as Identity from 'wata/identity'
import { Server } from 'wata/server'

const wata = Wata.create({
  baseUrl: 'https://wallet.example',
  identity: Identity.fromPrivateKey(privateKey),
  meta: { name: 'Example Wallet' },
  transports: [
    deviceCode({
      html: {
        async authenticate({ actions, request }) {
          const body = await request.formData()
          await actions.approve(String(body.get('user_code')))
          return new Response('approved')
        },
        render({ userCode }) {
          return new Response(
            `<form method="post"><input name="user_code" value="${userCode ?? ''}" required /><button>Approve</button></form>`,
            { headers: { 'content-type': 'text/html' } },
          )
        },
      },
      path: '/auth/device',
      store: Store.memory(),
    }),
  ],
})
const session = wata.start()

session.onRequest(async (c) => {
  if (c.method === 'wallet_connect')
    await c.respond(['0x0000000000000000000000000000000000000001'])
})

createServer(Server.node(wata).listener).listen(3000)
```

### `mobileWebAuth`

Same-device mobile flow where the consumer opens the host's HTTPS authorization URL in a system-browser auth session, then receives an app-link / private-scheme callback carrying the encrypted response.

[See example →](./examples/mobileWebAuth)

#### Consumer

Opens the host authorization URL with the platform's browser auth-session API. The auth session resolves with the callback URL, which the transport validates and decrypts.

```ts
import * as WebBrowser from 'expo-web-browser'
import { Wata } from 'wata/consumer'
import { mobileWebAuth } from 'wata/consumer/transports/mobileWebAuth'

const wata = Wata.create({
  baseUrl: 'https://app.example',
  meta: { name: 'Example App' },
  transports: [
    mobileWebAuth({
      callback: 'com.example.app://callback',
      openAuthSession: async ({ authorizationUrl, callback }) => {
        const result = await WebBrowser.openAuthSessionAsync(authorizationUrl, callback)
        if (result.type === 'success') return result.url
        return undefined
      },
    }),
  ],
})

const session = wata.start({ host: 'https://wallet.example' })

const { result } = await session.send({
  method: 'wallet_connect',
  params: [],
})
```

#### Host

Publishes `host.json`, renders an approval form at `/auth/mobile`, and redirects back to the consumer callback after approval.

```ts
import { createServer } from 'node:http'
import { Wata, mobileWebAuth } from 'wata/host'
import * as Identity from 'wata/identity'
import { Server } from 'wata/server'

const wata = Wata.create({
  baseUrl: 'https://wallet.example',
  identity: Identity.fromPrivateKey(privateKey),
  meta: { name: 'Example Wallet' },
  transports: [
    mobileWebAuth({
      html: {
        async authenticate({ actions, request }) {
          const body = await request.formData()
          return await actions.approve(String(body.get('state')))
        },
        render({ authorization }) {
          return new Response(
            `<form method="post">
              <input type="hidden" name="state" value="${authorization.state}" />
              <button>Approve</button>
            </form>`,
            { headers: { 'content-type': 'text/html' } },
          )
        },
      },
      path: '/auth/mobile',
    }),
  ],
})

const session = wata.start()

session.onRequest(async (event) => {
  if (event.method === 'wallet_connect')
    await event.respond(['0x0000000000000000000000000000000000000001'])
})

createServer(Server.node(wata).listener).listen(3000)
```

### `webhookCallback`

Server-to-server session where the consumer registers a signed intent with the host, sends the user to a verification URL, then receives the signed JSON-RPC response at its webhook endpoint.

[See example →](./examples/webhookCallback)

#### Consumer

Publishes `consumer.json`, serves a web page that starts the request, opens the host's verification URL for the user, then receives the callback response at its webhook endpoint.

```ts
import { Identity, Store, Wata, webhookCallback } from 'wata'

const wata = Wata.create({
  baseUrl: 'https://app.example',
  identity: Identity.fromPrivateKey(privateKey),
  meta: { name: 'Example App' },
  transports: [
    webhookCallback({
      path: '/callback',
      store: Store.memory(),
    }),
  ],
})

const session = wata.start({ host: 'https://wallet.example' })

session.onEnvelope((envelope, meta) => {
  if (envelope.type !== 'rpc-responses') return
  console.log(envelope.payload)
  console.log(meta)
})

const registration = await session.send({
  method: 'wallet_connect',
  params: [],
})

console.log(`Visit ${registration.verificationUri}`)
```

#### Host

Publishes `host.json`, accepts signed registrations, renders an approval form, and responds to approved requests.

```ts
import { Store, Wata, webhookCallback } from 'wata/host'
import * as Identity from 'wata/identity'

const wata = Wata.create({
  baseUrl: 'https://wallet.example',
  identity: Identity.fromPrivateKey(privateKey),
  meta: { name: 'Example Wallet' },
  transports: [
    webhookCallback({
      html: {
        async authenticate({ actions, request }) {
          const body = await request.formData()
          await actions.approve(String(body.get('code')))
          return new Response('approved')
        },
        render({ approvalToken, code, record }) {
          if (!record) return new Response('no pending request', { status: 404 })
          return new Response(
            `<form method="post">
              <input type="hidden" name="approval_token" value="${approvalToken ?? ''}" />
              <input type="hidden" name="code" value="${code ?? ''}" />
              <button>Approve</button>
            </form>`,
            { headers: { 'content-type': 'text/html' } },
          )
        },
      },
      path: '/auth/webhook',
      store: Store.memory(),
    }),
  ],
})
const session = wata.start()

session.onRequest(async (event) => {
  if (event.method === 'wallet_connect')
    await event.respond(['0x0000000000000000000000000000000000000001'])
})
```

### `relay`

Remote session between a web consumer and a mobile host, brokered by an untrusted HTTPS relay. The consumer issues a pairing link (typically shown as a QR code); the mobile host scans it and connects. All bodies are end-to-end encrypted, so the relay only forwards opaque ciphertext. Receivers default to SSE and can fall back to long-poll with `receive: 'poll'`.

[See example →](./examples/relay)

#### Consumer

Starts the session, reads the pairing `uri` from `session.prompt`, then sends once the host connects.

```ts
import { Wata, relay } from 'wata'

const wata = Wata.create({
  transports: [relay()],
})
const session = wata.start({ url: 'https://relay.example' })

// Render the pairing prompt (e.g. as a QR code) for the host to scan.
// `await session.ready` so the prompt is available; you can also subscribe
// to prompt changes with `session.onPrompt(...)`.
await session.ready
const prompt = session.prompt
if (prompt) renderQrCode(prompt.uri)

const { result } = await session.send({ method: 'ping', params: [] })
```

#### Host

Built once, then starts a session with the scanned/pasted pairing uri and registers request handlers on that session.

```ts
import { Wata, relay } from 'wata/host'

const wata = Wata.create({
  transports: [relay({ receive: 'poll' })],
})

// Start the session with the scanned/pasted pairing uri.
const session = wata.start({ uri })

session.onRequest((event) => event.respond('pong'))
```

#### Relay server

Run a relay anywhere that speaks web-standard `fetch` — a single long-lived Node/Bun/Deno process, or one Cloudflare Durable Object per channel.

```ts
import { createServer } from 'node:http'
import { Relay, Server, Store } from 'wata/server'

const relay = Relay.create({ store: Store.memory() })
createServer(Server.node(relay).listener).listen(8787)
```

### `mobileLink`

Direct, ongoing same-device session between two mobile apps over OS deep links. Feed OS-routed callbacks back in via `session.handleUrl(url)`.

[See example →](./playgrounds/mobileLink)

#### Consumer

Hands off to the host on the first request, then receives the encrypted, identity-verified response via the callback deep link.

```ts
import * as Linking from 'expo-linking'
import { Wata, mobileLink } from 'wata'

const wata = Wata.create({
  baseUrl: 'https://app.example',
  meta: { name: 'Example App' },
  transports: [
    mobileLink({
      async openLink(url) {
        await Linking.openURL(url)
      },
      returnUrl: 'com.example.app://cb',
    }),
  ],
})
const session = wata.start({ host: 'https://wallet.example' })

// Feed OS-routed callback deep links back into the transport.
Linking.addEventListener('url', ({ url }) => session.handleUrl(url))

const { result } = await session.send({ method: 'wallet_connect', params: [] })
```

#### Host

Verifies the inbound consumer, answers approved requests, and opens the consumer's `return_url` to deliver the signed response.

```ts
import * as Linking from 'expo-linking'
import { Identity, Wata, mobileLink } from 'wata/host'

const wata = Wata.create({
  baseUrl: 'https://wallet.example',
  identity: Identity.fromPrivateKey(privateKey),
  meta: { name: 'Example Wallet' },
  transports: [
    mobileLink({
      async openLink(url) {
        await Linking.openURL(url)
      },
      scheme: 'com.example.wallet',
    }),
  ],
})
const session = wata.start()

session.onRequest(async (event) => {
  if (event.method === 'wallet_connect')
    await event.respond(['0x0000000000000000000000000000000000000001'])
})

// Feed OS-routed inbound deep links back into the transport.
Linking.addEventListener('url', ({ url }) => session.handleUrl(url))
```

## Directory

#### Server

```ts
import { createServer } from 'node:http'
import { Directory, Server, Store } from 'wata/server'

const store = Store.memory()
const origins = ['https://wallet.example', 'https://other.example']

// Index the seed list now, then keep it fresh (run on a cron in production).
await Directory.crawl({ origins, store })
setInterval(() => Directory.crawl({ origins, store }), 60 * 60 * 1000)

// Serve `/v1/hosts` — `create` returns a web-standard `{ fetch }` handler.
const directory = Directory.create({ store })
createServer(Server.node(directory).listener).listen(8788)
```

On Cloudflare, back it with KV and run the crawl from a cron trigger:

```ts
import { Directory, Store } from 'wata/server'

const origins = ['https://wallet.example']

export default {
  fetch(request: Request, env: Env) {
    return Directory.create({ store: Store.cloudflare(env.DIRECTORY_KV) }).fetch(request)
  },
  scheduled(_event: ScheduledEvent, env: Env) {
    return Directory.crawl({ origins, store: Store.cloudflare(env.DIRECTORY_KV) })
  },
}
```

#### Query

```ts
import { Discovery, Wata, relay } from 'wata'

// 1. Discover relay-capable hosts.
const response = await fetch('https://directory.example/v1/hosts?transport=relay')
const { items } = await response.json()

// 2. Validate the candidate against its own host.json.
const host = await Discovery.fetchHost(items[0].origin)

// 3. Create the consumer (relay URL and host deep link supplied at start).
const wata = Wata.create({
  transports: [relay()],
})

// 4. Start a session over the validated host's advertised relay, targeting
//    the host's `deep_link` so the pairing link opens the host app, then
//    render the prompt for the host to scan.
const session = wata.start({
  target: host.deep_link?.universal_link ?? host.deep_link?.scheme,
  url: host.transports.relay.url,
})
await session.ready
if (session.prompt) renderQrCode(session.prompt.uri)

const { result } = await session.send({ method: 'ping', params: [] })
```

## License

[MIT](./LICENSE). Copyright © wevm.

---
_Source: https://npm.io/package/wata · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
