# Embedded Wallet

> Coming soon, upon request — this feature is turned on for your tenant by the Aurea team. [Contact us](https://www.aureahub.com/contact)

Show your users' MPC wallets inside your own web pages with `@aureahub/wallet-embed`: the wallet frame keeps the device share, the passkey and the approval screens on its own origin, and your server carries its calls to the API. Your API integration stays exactly as it is.

## Two Ways to Integrate

|  |  |  |
| --- | --- | --- |
| API (every tenant, the default) | — | Your app runs the MPC client: it holds the user's device share and takes part in each ceremony through `/v1/mpc` ([MPC Wallet Setup](https://docs.aureahub.com/docs/guide-wallet-setup.md)). Nothing changes for a tenant that stays on the API. |
| Embedded wallet + API (on request, per tenant) | — | Your web pages show the wallet frame from `https://vault.aureahub.com`. The frame runs the ceremonies, keeps the device share in the user's browser and asks the user's passkey for every signature. Your server carries the frame's calls to the same `/v1/mpc` routes with your user's session. Every other API call of your integration works as before, side by side. |

## Turning It On

1. Write to [help@aureahub.com](mailto:help@aureahub.com?subject=Embedded%20wallet) to turn the embedded wallet on for your tenant, with the **exact origins** of the pages that will show it, for sandbox and for production.
2. You receive one **publishable key** per environment: `pk_test_…` for sandbox, `pk_live_…` for production, with the frame's origin to use for each (production: `https://vault.aureahub.com`). The key is public: it goes in your page.
3. The origins are published with the next release of the wallet frame; we tell you when. From then on the frame opens only on those origins, and only with that key.

Origin rules: an exact origin — scheme, host and port only, no path, no trailing slash, no wildcard; `https` (plain `http` only for `localhost`, `*.localhost` and `*.test`, on a sandbox key); at most 10 per key. A change of origins, or a key revoked, takes effect with the next release of the frame the same way. Turning the embedded wallet off again leaves your wallets and your API integration as they are; the frame no longer opens on your pages.

## Install

```bash
npm install @aureahub/wallet-embed
```

No dependencies; ESM with TypeScript types; runs in the browser. The package is the page's side of the frame's channel: it mounts the frame, runs the handshake, checks the channel binding and exchanges typed requests. It never receives a key share, a passkey output or a recovery code — the channel has no message that carries one.

## Your Server Carries the Calls

The frame has no network of its own. For every ceremony step it asks your page to carry a call to the API, and your page hands it to your server, which sends it with your user's access token and your tenant's request signature ([Authentication](https://docs.aureahub.com/docs/authentication.md)), exactly as your other `/v1/mpc` calls. Carry only the wallet paths: `isAllowedHubPath` says which.

```typescript
// In your page
import { createVaultClient } from '@aureahub/wallet-embed'

const vault = createVaultClient({
  vaultOrigin: 'https://vault.aureahub.com',
  pk: 'pk_live_EXAMPLE',                  // your publishable key
  hub: async (req) => {                   // { method, path, body } — path under /v1/mpc
    const r = await fetch('/wallet-calls' + req.path, {
      method: req.method,
      credentials: 'include',             // your own session with your server
      headers: { 'Content-Type': 'application/json' },
      ...(req.method === 'POST' && req.body !== undefined ? { body: req.body } : {}),
    })
    return { status: r.status, body: r.status === 204 ? '' : await r.text() }
  },
})

// On your server (any framework): /wallet-calls/* → https://api.aureahub.com/v1/mpc/*
import { isAllowedHubPath } from '@aureahub/wallet-embed'

app.all('/wallet-calls/*', async (req, res) => {
  const path = req.originalUrl.slice('/wallet-calls'.length)        // e.g. /wallets/dkg/start
  if (!isAllowedHubPath(path)) return res.status(403).end()
  const body = req.method === 'POST' ? JSON.stringify(req.body) : undefined
  const answer = await fetch('https://api.aureahub.com/v1/mpc' + path, {
    method: req.method,
    headers: {
      Authorization: `Bearer ${userAccessToken(req)}`,             // the signed-in user's Aurea session
      ...signRequest(req.method, '/v1/mpc' + path, body),             // your x-tenant-api-key, x-timestamp, x-signature
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body,
  })
  res.status(answer.status).type('application/json').send(await answer.text())
})
```

## Create a Wallet

`createWallet` runs the creation in the frame: the user makes a passkey, the key generation gives the address in seconds, then the second ceremony and the backup run in the frame while the user writes down a recovery code that only the frame shows.

```typescript
import { createWallet, VaultError } from '@aureahub/wallet-embed'

const wallet = createWallet({
  vault,
  tenantPk: 'pk_live_EXAMPLE',
  setupStatus: () => myServer.walletSetups(),   // optional: GET /v1/mpc/wallets/setup through your server
})
wallet.on('wallet.setup.backup_confirmed', (e) => showDepositAddress(e.address))

try {
  const { walletId, address, setup } = await wallet.create()   // the address, in seconds
  const answer = await setup                                    // minutes: second ceremony + backup
  if (answer) await myServer.keepBackup(walletId, answer.backup) // sealed: keep it with the user's record
} catch (e) {
  if (e instanceof VaultError) offer(e.next)                    // every refusal carries its next step
}
```

- **Backup before funding.** The device share lives in the user's browser, in storage the browser keeps for your site only. Show the address for deposits only from `wallet.setup.backup_confirmed` (or a setup view with `canReceive: true`); signing waits for the second ceremony (`canSpend`).
- **Keep the backup.** The answer's `backup` is sealed under the recovery code the user wrote down and, where the passkey allows, under the passkey: store it with the user's record. You cannot open it; it is what recovery needs.
- **Resume.** With `setupStatus`, a setup the user left is continued instead of starting a second wallet (one wallet in setup per user: `409` `MPC_WALLET_SETUP_PENDING` otherwise, [Restart a Pending Wallet](https://docs.aureahub.com/docs/mpc-wallet-restart.md)).
- **Events** (`wallet.on(type | '*', fn)`): `wallet.created`, `wallet.setup.aux_started`, `wallet.setup.ready_for_backup`, `wallet.setup.backup_confirmed`, `wallet.setup.completed`, `wallet.setup.retired`, `account.bound`, and `wallet.setup.failed` with its `code` and `next`. Your server receives the same setup events as webhooks.

## Sign

Send what the user is asked to sign as an **intent**: the frame shows it in words, with your origin as the requester, and the user confirms with the passkey. The answer is the signature (`0x` ‖ r ‖ s ‖ yParity, 65 bytes); nothing is broadcast — your server sends the transaction as today.

```typescript
const { signature } = await vault.request('tx.sign', {
  walletId,
  intent: {
    v: 1, kind: 'eip1559', chainId: '8453',
    tx: { nonce: '7', to: '0x…', value: '0', gas: '90000', maxFeePerGas: '2000000000',
          maxPriorityFeePerGas: '1000000', accessList: [], data: '0xa9059cbb…' },
  },
  display: { merchant: 'Your shop', note: 'Order 1042' },   // optional, shown in the frame
})

// A message (EIP-191) or typed data (EIP-712):
await vault.request('message.sign', { walletId, intent: { v: 1, kind: 'eip191', message: '0x48656c6c6f' }, display: {} })
await vault.request('typed.sign', { walletId, intent: { v: 1, kind: 'eip712', /* domain, types, message */ }, display: {} })
```

## Recovery and Backup Proof

- **Another browser or cleared data**: `vault.request('wallet.recover', { walletId, address, file, passkeyBackup })` with the backup you kept. The user types the recovery code in the frame (or uses the passkey); the wallet then signs from its backup, with the owner's step-up ([Recovery Step-Up](https://docs.aureahub.com/docs/mpc-step-up.md)).
- **Proof that the backup works**: `vault.request('backup.prove', { walletId, file, passkeyBackup })` runs the backup proof in the frame (moves no funds).

## Safari and Other Browsers

Where a browser does not allow passkeys inside a frame, a request answers `passkey_unavailable` with the next step `open_popup`: call `vault.openPopup()` from the user's click, and the same requests then go to the frame's own window. Safari keeps that window's storage apart from the frame's, so use one of the two consistently for a user.

## Security Model

- **Its own origin.** The device share, the passkey and its output, the recovery code and the approval screens stay on `https://vault.aureahub.com`. Your page — and anything injected into it — never reads them.
- **Your origins only.** The browser shows the frame only on the origins registered for your key, and the frame opens a channel only with a page on one of them that names that key. Another site's page, or your page with another tenant's key, is refused.
- **Wallet requests only.** Your page may ask for the wallet's status, creation, signatures, recovery and backup proof; nothing else.
- **One network path.** The frame's only calls are the wallet paths your server carries with your user's session: the API still checks the user, the wallet's owner and your tenant on every call.
- **The user approves what is signed.** Each signature carries the intent and the passkey's approval of that exact intent; the platform's co-signer refuses a signature whose approval or intent does not match.

## Which Way a Wallet Is Used

- **Through the API, any wallet.** A wallet made in the frame is an ordinary MPC wallet of your user: listed, its balances, history, deposits, setup states and webhooks — everything your server reads or receives — work the same for both.
- **Signing, where its device share is.** A wallet made in the frame signs in the frame, on your site, with the user's passkey; a wallet made by your app signs in your app. The device share is never exported, and the frame does not take one from your app, so a wallet keeps signing the way it was created.
- **A new device or browser.** An embedded wallet is recovered in the frame with the backup you kept; a wallet of your app with your app's own recovery ([MPC Wallet Setup](https://docs.aureahub.com/docs/guide-wallet-setup.md)).

## Errors

A refused request throws `VaultError` with `code` and `next` (the step to offer the user); a frame that did not load throws `VaultUnavailableError`.

|  |  |  |
| --- | --- | --- |
| `origin_refused` | — | The page's origin is not registered for this key, or the request is not one a site may make. Check the origin and the key; write to help@aureahub.com to register an origin. |
| `passkey_unavailable` | — | This browser does not allow passkeys in a frame. `open_popup`: `vault.openPopup()` from a click. |
| `user_cancelled` | — | The user closed or declined the passkey or the screen. `retry`. |
| `locked` | — | The wallet in this browser is locked. `unlock`: `wallet.unlock`. |
| `not_found` | — | This browser holds no such wallet (another browser, cleared data). `recover`: `wallet.recover` with the backup. |
| `refused` | — | The API or the co-signer refused the step. The `next` it carries — for a setup, `resume_setup` with the pending wallet. |
| `malformed` | — | A body the channel does not accept (a field, an intent). Fix the request. |
| `unsupported_version` | — | The page's package and the frame share no protocol version. `update_app`: update `@aureahub/wallet-embed`. |
| `engine_error` | — | The ceremony failed in the frame. `retry`, then `contact_support`. |

---

Web version: https://docs.aureahub.com/#guide-embedded-wallet
