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

# @ave-id/sdk App encryption

> Per-app symmetric keys and asymmetric keypairs for multi-user E2EE without OAuth handshakes.

```bash theme={null}
bun add @ave-id/sdk
```

## Encryption scopes

Enable encryption by requesting an `e2ee:*` scope at sign-in. No developer-portal configuration is required.

| Scope             | Mode               | Fragment parameters                        |
| ----------------- | ------------------ | ------------------------------------------ |
| `e2ee:symmetric`  | AES-256 app key    | `#app_key=`                                |
| `e2ee:asymmetric` | ECDH P-256 keypair | `#app_public_key=` and `#app_private_key=` |

Request exactly one E2EE mode scope per sign-in. Re-sign-in reuses existing per-app keys unless you also request `e2ee:reset`:

```ts theme={null}
import { buildAuthorizeUrl } from "@ave-id/sdk";

const url = buildAuthorizeUrl(
  { clientId, redirectUri, issuer: "https://aveid.net" },
  { scope: ["openid", "profile", "email", "e2ee:asymmetric"] },
);

// Dangerous: rotate keys (also shows a warning on the Ave consent screen)
const rotateUrl = buildAuthorizeUrl(
  { clientId, redirectUri, issuer: "https://aveid.net" },
  { scope: ["openid", "profile", "email", "e2ee:asymmetric", "e2ee:reset"] },
);
```

After consent, parse the fragment with `mergeAppEncryptionFromUrl` (PKCE) or `completeOAuthCallback` (session helper). Store the **private key locally** in your app; persist the **public key** on your user record.

On key rotation (`e2ee:reset`), the fragment also includes previous keys (`#app_key_old`, `#app_public_key_old`, `#app_private_key_old`) and `#app_key_reset=true` so your app can migrate ciphertext before discarding the old material.

## Multi-user encryption (asymmetric)

Look up another user's app public key, encrypt for them, and they decrypt with the private key you already stored locally.

```ts theme={null}
import {
  encryptForAppHandle,
  decryptFromAppSender,
  lookupAppUserByPublicKey,
} from "@ave-id/sdk";

const wrapped = await encryptForAppHandle("room secret", {
  clientId,
  issuer: "https://aveid.net",
  handle: "alice",
});

const alice = await lookupAppUserByPublicKey(
  { clientId, issuer: "https://aveid.net" },
  storedPublicKey,
);

const plain = await decryptFromAppSender(
  wrapped,
  session.getAppPrivateKeyBase64()!,
);
```

### Lookup API

Public endpoint (no user token). Returns **404** if the handle is unknown, the user has never signed in to your app, or they have not provisioned an app encryption key — your app should show messaging like “this person hasn’t used your app yet” instead of generating keys server-side.

* `GET /api/encryption/app-lookup?client_id=…&handle=…` → public key + user metadata
* `GET /api/encryption/app-lookup?client_id=…&public_key=…` → user metadata for that key

```ts theme={null}
import {
  lookupAppPublicKeyByHandle,
  AppEncryptionLookupError,
} from "@ave-id/sdk";

try {
  const recipient = await lookupAppPublicKeyByHandle(
    { clientId, issuer: "https://aveid.net" },
    "alice",
  );
} catch (error) {
  if (error instanceof AppEncryptionLookupError && error.status === 404) {
    // e.g. "Alice hasn't signed in to this app yet"
  }
}
```

## Symmetric mode (legacy-compatible)

Older apps with `supportsE2ee` still default to symmetric when no `e2ee:*` scope is requested. The flow is unchanged: `#app_key=` in the redirect fragment.

## Removed: identity wrapped OAuth payloads

The old `wrapped_key` / `unwrapped_secret` authorize flow is removed. It required per-identity encryption keys and sent users back through Ave for every invite. Use app keypairs and the lookup API instead.

[Ave Signing](/sdk/sdk-signing) (Ed25519) is unchanged and separate from app encryption.
