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

> ## Agent Instructions
> These docs target Mesh SDK v0.27.0. Match the installed package and binding before generating code. Start at /getting-started/agents for task-specific reading paths.
> Call the company and product Offline Protocol, never Offline alone. Current packages: @offline-protocol/mesh-sdk 0.27.0 (React Native), @offline-protocol/id-react 0.2.0, @offline-protocol/id-react-native 0.3.3, @offline-protocol/pol 0.1.2 and @offline-protocol/cli 0.2.6. Canonical docs URLs start with https://www.offlineprotocol.com/docs.
> The Mesh SDK runs in a native app or gateway. A browser OfflineID SDK integration does not provide browser mesh transport. Local mesh operation does not require a portal API key.
> Service RPC is signed plaintext in v0.27.0. Message delivery, durable local acceptance and backend commit are distinct outcomes. Use the workflow guide for the required application logic.
> Offline Protocol CLI 0.2.6 is on npm (@offline-protocol/cli, command offline). Its local MCP server runs with offline mcp serve and requires no login or key. Hosted MCP is at https://mcp.offlineprotocol.com/mcp with an application API key in Authorization: Bearer and a matching x-app-id; organization keys are rejected. Follow /tools/overview for setup and do not invent commands beyond it. MCP provides integration context and planning, not mesh execution; file-writing tools are local only.
> Phone Wi-Fi Direct and MultipeerConnectivity carry no data in v0.27.0. Use BLE or a provisioned relay. The receiver core ACKs before application persistence; use application acceptance for durable workflows.
> Proof of Location is Sepolia testnet witness evidence, not zero-knowledge proof or proof of presence. The geohash is public onchain. Read /proof-of-location/security before integration.

# Device identity and sessions

> Read device addresses, manage key packages and establish MLS sessions.

Import `OfflineProtocol` from `@offline-protocol/mesh-sdk`. These signatures match **v0.27.0**. Read [the integration guide](/docs/mesh-sdk/identity) for prerequisites and complete usage.

## Methods on this page

* [`initializeMlsWithSecureStorage`](#initializemlswithsecurestorage)
* [`isMlsInitialized`](#ismlsinitialized)
* [`getIdentityPublicKey`](#getidentitypublickey)
* [`deriveAddress`](#deriveaddress)
* [`parseInvite`](#parseinvite)
* [`createInvite`](#createinvite)
* [`resolveUsername`](#resolveusername)
* [`localAddress`](#localaddress)
* [`deriveUserIdFromPublicKey`](#deriveuseridfrompublickey)
* [`signData`](#signdata)
* [`verifySignature`](#verifysignature)
* [`mlsGenerateKeyPackage`](#mlsgeneratekeypackage)
* [`mlsGetOrCreateKeyPackage`](#mlsgetorcreatekeypackage)
* [`mlsGetPendingKeyPackages`](#mlsgetpendingkeypackages)
* [`mlsMarkKeyPackageSynced`](#mlsmarkkeypackagesynced)
* [`mlsImportKeyPackage`](#mlsimportkeypackage)
* [`mlsHasSession`](#mlshassession)
* [`hasPendingKeyPackage`](#haspendingkeypackage)
* [`getEstablishmentState`](#getestablishmentstate)
* [`establishSecureSession`](#establishsecuresession)
* [`rekeySession`](#rekeysession)
* [`mlsCreateSession`](#mlscreatesession)
* [`mlsJoinSession`](#mlsjoinsession)
* [`mlsEncryptForUser`](#mlsencryptforuser)
* [`mlsDecryptFromUser`](#mlsdecryptfromuser)
* [`mlsDecrypt`](#mlsdecrypt)
* [`mlsListSessions`](#mlslistsessions)
* [`mlsDeleteSession`](#mlsdeletesession)
* [`mlsProcessWelcome`](#mlsprocesswelcome)

## initializeMlsWithSecureStorage

Initializes MLS with built-in secure storage.
Uses iOS Keychain or Android EncryptedSharedPreferences.

**Note:** This is called automatically by `start()` when `encryption.enabled` is true (default).
You only need to call this manually if you disabled encryption initially and want to enable it later.

**throws:** Error if initialization fails

```typescript theme={null}
initializeMlsWithSecureStorage(): Promise<void>
```

## isMlsInitialized

Checks if MLS is initialized.

**returns:** True if MLS is ready for use

```typescript theme={null}
isMlsInitialized(): Promise<boolean>
```

## getIdentityPublicKey

Gets the identity public key (Ed25519, 32 bytes).
This is the public key derived from the MLS credential and can be shared
with others for identity verification and secure communication.

**returns:** The public key as an array of bytes

**throws:** Error if MLS is not initialized

```typescript theme={null}
getIdentityPublicKey(): Promise<number[]>
```

## deriveAddress

Derives the canonical self-certifying address of an Ed25519 identity key:
`off1…` (44 characters), the bech32m encoding of
`0x01 || SHA-256(publicKey)[0:20]`.

The address is a hash of the key, so it authenticates itself: a peer
claiming an address is checked by re-deriving it from the key they
present. The same key always yields the same address, and every address
has exactly one valid string form.

Needs no protocol instance: safe to call before `create()`, e.g. to
verify an invite or QR code.

**returns:** The derived `off1…` address

**throws:** If `publicKey` is not 32 bytes

```typescript theme={null}
deriveAddress(publicKey: number[]): Promise<string>
```

## parseInvite

Decodes and verifies an invite blob.

Needs no protocol instance: safe to call before `create()`, which is the
whole point: a scanner verifies a QR code before deciding to act on it.

Verification is mandatory and total. The address must be the one its
public key derives to, and any signature present must verify under that
key, so a resolved `InviteInfo` is always self-certified.

**What it does not prove:** that the invite came from who you think. An
attacker's own correctly-signed invite is indistinguishable from a
legitimate stranger's: only the out-of-band context (this QR was on
*this* person's screen) carries that.

**returns:** The verified invite

**throws:** If the blob is malformed, the address is not the key's, or a
signature does not verify. Every case means refuse, not warn.

```typescript theme={null}
parseInvite(blob: string): Promise<InviteInfo>
```

## createInvite

Builds an invite blob for this identity.

The result is opaque base64url. Apps own the container; the recommended
form is one parameter, `<app-scheme>://connect?c=<blob>`, so it composes
with an existing scheme and route.

Sign it when the invite may travel **without its issuer**, a link
forwarded through a third party, because the signature binds the petname
to the key, so a forwarded invite cannot save Alice's key under the name
"Bob". Leave it unsigned for a QR shown phone to phone: the physical
channel already authenticates it, and an app that lets the user confirm
the name has made the user the authority over it. Signing costs about 90
characters.

Carries no key package by design (an MLS init key is single-use and a QR
code is static, so pairing them guarantees a collision as soon as two
people scan the same code) and no expiry (a printed QR that stops working
is a bug).

```typescript theme={null}
createInvite(petname?: string, signed = false): Promise<string>
```

## resolveUsername

Returns true if this call started the lookup, false if one was already running; exactly one username\_resolved follows either way.

Requires `transports.nostr.usernameDiscoveryEnabled`. Resolves `true` if
this call started the lookup and `false` if it joined one already in
flight. **Both mean an answer is coming**: exactly one
`username_resolved` event follows either way, so awaiting that event after
either result is safe.

Every case where no event will ever arrive **rejects** instead, so a
`false` can never leave a spinner running forever. The rejection code says
which, and they call for different handling:

* `InvalidConfiguration`: discovery is off (it also requires
  `coldContactEnabled`). Retrying unchanged can never succeed.
* `InvalidState`: too many lookups in flight. Transient; retry shortly.
* `NotStarted`: the protocol is not running, so nothing would pump relay
  traffic or sweep the deadline for this lookup. Transient; retry after
  `start()`.
* `InvalidArgument`: not a claimable username (empty, over 64 bytes once
  normalized, carrying a control or format character, or address-shaped).

Subscribe to `username_resolved` **before** calling this. The answer is a
single event with no replay, so a listener attached afterwards can miss it.

The answer carries **every** verified claim. There is deliberately no
"best" claim and no ordering: anyone may publish any name, so what comes
back is a set of assertions for a human to arbitrate, not a lookup result.

**Do not auto-select.** Taking the first entry turns a non-authoritative
directory into an authoritative-looking one: the user then believes the
*name* was verified when only a key ever was. Present the claims, have the
user confirm out of band, and store the address, never the name: a name
can be re-claimed by anyone tomorrow, an address is self-certifying.

**returns:** `true` if this call started the lookup, `false` if it joined one

**throws:** `InvalidConfiguration` if discovery is off, `InvalidState` if too
many lookups are in flight, `NotStarted` if the protocol is not running,
`InvalidArgument` if the name is not claimable

```typescript theme={null}
resolveUsername(username: string): Promise<boolean>
```

## localAddress

This device's own address (`off1…`), or `null` before startup completes.

Derived from the identity key held in this profile's storage: the app
does not choose it, and it is stable across restarts of the same
`profile`. This is the string to show the user, put in an invite or QR
code, and what peers pass as the recipient to reach this device.

`null` until MLS is initialized (which `start()` does), because the key
that defines it lives in storage that is not open before then. The
`identity_ready` event carries the same value at the moment it is known.

```typescript theme={null}
localAddress(): Promise<string | null>
```

## deriveUserIdFromPublicKey

Derives a deterministic user ID from a public key.

**deprecated:** Use `deriveAddress`. This returns the same `off1…`
address, but requires an initialized protocol instance and accepts any
input length rather than rejecting keys that are not 32 bytes.

**returns:** The derived address string

```typescript theme={null}
deriveUserIdFromPublicKey(publicKey: number[]): Promise<string>
```

## signData

Signs arbitrary data with the identity private key (Ed25519).
Use this to prove ownership of your identity or to sign messages.

**returns:** The signature as an array of bytes (64 bytes)

**throws:** Error if MLS is not initialized

```typescript theme={null}
signData(data: number[]): Promise<number[]>
```

## verifySignature

Verifies a signature against a public key.
Use this to verify that data was signed by the owner of a public key.

**returns:** True if the signature is valid

**throws:** Error if verification fails due to invalid input

```typescript theme={null}
verifySignature(publicKey: number[], data: number[], signature: number[]): Promise<boolean>
```

## mlsGenerateKeyPackage

Generates a new MLS key package.
Key packages are used by others to establish encrypted sessions with you.

**returns:** Generated key package

**throws:** Error if generation fails

```typescript theme={null}
mlsGenerateKeyPackage(): Promise<MlsKeyPackage>
```

## mlsGetOrCreateKeyPackage

Gets an existing key package or creates a new one.

**returns:** Key package

**throws:** Error if operation fails

```typescript theme={null}
mlsGetOrCreateKeyPackage(): Promise<MlsKeyPackage>
```

## mlsGetPendingKeyPackages

Gets pending key packages that haven't been synced yet.

**returns:** Array of pending key packages

```typescript theme={null}
mlsGetPendingKeyPackages(): Promise<MlsKeyPackage[]>
```

## mlsMarkKeyPackageSynced

Marks a key package as synced.

**throws:** Error if operation fails

```typescript theme={null}
mlsMarkKeyPackageSynced(packageId: string): Promise<void>
```

## mlsImportKeyPackage

Imports another user's key package.
Required before you can send encrypted messages to them.

**throws:** Error if import fails

```typescript theme={null}
mlsImportKeyPackage(userId: string, keyPackageData: number[]): Promise<void>
```

## mlsHasSession

Checks if an MLS session exists with another user.

**returns:** True if session exists

```typescript theme={null}
mlsHasSession(otherUserId: string): Promise<boolean>
```

## hasPendingKeyPackage

Checks if a pending key package is available for a peer.

Key packages are received automatically when peers are discovered
(if auto\_key\_exchange is enabled). This method checks if we have
received the peer's key package and can establish a session.

**returns:** True if key package is available

```typescript theme={null}
hasPendingKeyPackage(peerId: string): Promise<boolean>
```

## getEstablishmentState

Returns the current secure-session establishment state for a peer.

Useful for retry/UI flows when operations fail with `SessionNotReady`.

```typescript theme={null}
getEstablishmentState(peerId: string): Promise<EstablishmentState>
```

## establishSecureSession

Establishes a secure MLS session with a peer (high-level API).

This method handles the complete session establishment flow:

* If session already exists, returns null
* If a pending key package is available, imports it, creates session, sends Welcome
* If no key package is available, throws an error

This is the recommended method for establishing secure sessions as it
handles the key package exchange flow automatically.

**returns:** Welcome message if session was created, null if session already exists

**throws:** Error if no key package is available (peer hasn't completed key exchange)

```typescript theme={null}
establishSecureSession(peerId: string): Promise<MlsWelcome | null>
```

## rekeySession

Rotate the 1:1 session with a peer, advancing post-compromise security.

Post-compromise security arrives when a commit rotates a member's leaf in
the ratchet tree, and the SDK originates one on a re-key. Nothing drives a
re-key on its own except an epoch desync, so a pair that never forks never
rotates unless the application asks. That bites hardest against a leaf
node, a lock, a sensor, which never commits at all, so every rotation in
such a pair is this side's to originate.

The cadence is yours on purpose: a rotation costs a teardown, a key-package
exchange and a re-establish, and what that is worth depends on the
deployment rather than on anything the wire says.

The peer sees exactly what a desync-driven re-key sends. Queued messages
survive, because they are sealed at flush time against whatever session is
current then.

A rotation that fails changes nothing. The reset is advertised before the
local session is torn down, so a rejection leaves the session intact,
still usable, and the rate-limit window unspent. Rotate while the peer is
reachable and treat a failure as "try again later" rather than as a
session to rebuild.

**returns:** true when the rotation was driven; false when the per-peer
rate-limit window has not lapsed. False is not a failure: call again
later.

**throws:** Error if encryption is not initialized, the peer is blocked, there
is no session to rotate (establish one first), or no transport carried
the reset.

```typescript theme={null}
rekeySession(peerId: string): Promise<boolean>
```

## mlsCreateSession

Creates an MLS session with another user.
Returns a Welcome message that must be sent to the other user.

Note: Prefer using `establishSecureSession` which handles the key package
flow automatically. This lower-level method requires the peer's key package
to already be imported via `mlsImportKeyPackage`.

**returns:** Welcome message to send to the other user

**throws:** Error if session creation fails

```typescript theme={null}
mlsCreateSession(otherUserId: string): Promise<MlsWelcome>
```

## mlsJoinSession

Joins an MLS session from a Welcome message.

**returns:** Session info

**throws:** Error if joining fails

```typescript theme={null}
mlsJoinSession(welcome: MlsWelcome): Promise<MlsSessionInfo>
```

## mlsEncryptForUser

Encrypts a message for another user.
Creates a session automatically if one doesn't exist.

**returns:** Encrypted message

**throws:** Error if encryption fails

```typescript theme={null}
mlsEncryptForUser(otherUserId: string, plaintext: number[]): Promise<MlsEncryptedMessage>
```

## mlsDecryptFromUser

Decrypts a message from another user.

**returns:** Decrypted plaintext as bytes, or null if decryption fails

```typescript theme={null}
mlsDecryptFromUser(encrypted: MlsEncryptedMessage): Promise<number[] | null>
```

## mlsDecrypt

Decrypts any MLS message (1:1 or group).

**returns:** Decrypted plaintext as bytes, or null if decryption fails

```typescript theme={null}
mlsDecrypt(encrypted: MlsEncryptedMessage): Promise<number[] | null>
```

## mlsListSessions

Lists all active MLS sessions.

**returns:** Array of user IDs with active sessions

```typescript theme={null}
mlsListSessions(): Promise<string[]>
```

## mlsDeleteSession

Deletes an MLS session with another user.

**throws:** Error if deletion fails

```typescript theme={null}
mlsDeleteSession(otherUserId: string): Promise<void>
```

## mlsProcessWelcome

Processes a Welcome message (auto-detects session vs group).

**returns:** Session or group info

**throws:** Error if processing fails

```typescript theme={null}
mlsProcessWelcome(welcome: MlsWelcome): Promise<MlsSessionInfo | MlsGroupInfo>
```

## Parameter and result types

Import these types from the same SDK package. Event payloads and configuration types are linked below.

### InviteInfo

```typescript theme={null}
export interface InviteInfo {
  address: string;
  public_key: number[];
  petname: string | null;
  signed: boolean;
}
```

### MlsKeyPackage

```typescript theme={null}
export interface MlsKeyPackage {
  packageId: string;
  userId: string;
  keyPackageData: number[];
  createdAt: number;
  isSynced: boolean;
}
```

### EstablishmentState

```typescript theme={null}
export type EstablishmentState =
  | 'NoKeyPackage'
  | 'HaveKeyPackage'
  | 'SessionPending'
  | 'SessionConfirmed';
```

### MlsWelcome

```typescript theme={null}
export interface MlsWelcome {
  groupId: string;
  welcomeData: number[];
  inviterId: string;
  timestampMs: number;
}
```

### MlsSessionInfo

```typescript theme={null}
export interface MlsSessionInfo {
  otherUserId: string;
  groupId: string;
  epoch: number;
  createdAt: number;
}
```

### MlsEncryptedMessage

```typescript theme={null}
export interface MlsEncryptedMessage {
  groupId: string;
  messageType: string;
  epoch: number;
  ciphertext: number[];
  senderId: string;
  timestampMs: number;
}
```

### MlsGroupInfo

```typescript theme={null}
export interface MlsGroupInfo {
  groupId: string;
  groupName: string;
  memberIds: string[];
  epoch: number;
  createdAt: number;
}
```

## Related references

* [`identity_ready`](/docs/mesh-sdk/events#identity_ready)
* [`secure_session_established`](/docs/mesh-sdk/events#secure_session_established)
* [Event payloads](/docs/mesh-sdk/events)
* [Configuration types](/docs/mesh-sdk/configuration-types)
* [All public types](/docs/mesh-sdk/public-types)
* [Error codes](/docs/mesh-sdk/errors)
* [All API tasks](/docs/mesh-sdk/reference)
