Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0a5d34d
feat(host-papp): add V2 SSO handshake codec, state machine, and pairi…
kalininilya May 20, 2026
3f64e60
docs(host-papp): document the V2 SSO handshake
kalininilya Apr 29, 2026
43163a6
feat(host-papp): carry identity chat priv key in V2 SSO success payload
kalininilya May 1, 2026
5c0514e
fix(host-papp): make V2 chat priv key optional for legacy PApp builds
kalininilya May 1, 2026
f5dfe43
feat(host-papp): replace V2 SSO encryption_key with identity_chat_pri…
kalininilya May 1, 2026
659413e
feat(host-papp): adopt multi-device HandshakeSuccessV2 v0.2.1 wire shape
kalininilya May 19, 2026
1d1c292
fix(host-papp,host-chat): tolerate camelCase Resources.Consumers fields
kalininilya May 19, 2026
72e0323
feat(host-chat): add multi-device chat content variants (indices 14–20)
kalininilya May 19, 2026
6bd3823
chore: refresh package-lock.json for new V2 SSO deps
kalininilya May 20, 2026
4c0fdc1
refactor(host-papp): drive V2 SSO inside createAuth, drop V1 handshake
kalininilya May 21, 2026
692943a
docs(changelog): reframe 0.8.0 entry against the 0.7.8 baseline
kalininilya May 21, 2026
2a21cfe
refactor(host-papp): absorb device-identity + persistence into the SD…
kalininilya May 21, 2026
094e4ce
feat(host-chat): add paseo-next-v2 network config
kalininilya May 22, 2026
107c7fb
fix(host-chat): drop private flag so the package is publishable
kalininilya May 22, 2026
e6d6be8
docs(host-chat): replace stale host-container copy with a real README
kalininilya May 22, 2026
0c755a3
Merge branch 'release/0.8' into fix/publish-host-chat
johnthecat May 22, 2026
57db32c
docs: changelog
johnthecat May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 68 additions & 263 deletions packages/host-chat/README.md
Original file line number Diff line number Diff line change
@@ -1,297 +1,102 @@
# @novasamatech/host-container
# @novasamatech/host-chat

A robust solution for hosting and managing decentralized applications (dapps) within the Polkadot ecosystem.
Account lookup and chat-message codecs for host applications integrating with the Polkadot People chain.

## Overview

Host container provides the infrastructure layer for securely embedding and communicating with third-party dapps.
It handles the isolation boundary, message routing, lifecycle management, and security concerns inherent in hosting untrusted web content.
`@novasamatech/host-chat` exposes the read side of the chat domain: discovering Polkadot
accounts by username and resolving their on-chain identity from `Resources.Consumers`. It
also publishes the SCALE codecs used by the chat wire protocol (messages, attachments,
local-message envelopes) so host applications can decode statements they receive over the
statement store.

The package is UI-framework agnostic. The main entry point returns plain async functions
backed by [`neverthrow`](https://github.com/supermacro/neverthrow) `ResultAsync`, and the
codec exports are pure SCALE codecs with no runtime side effects.

## Installation

```shell
npm install @novasamatech/host-container --save -E
```

### Basic Container Setup

```ts
import { createContainer, createIframeProvider } from '@novasamatech/host-container';

const iframe = document.createElement('iframe');

const provider = createIframeProvider({
iframe,
url: 'https://dapp.example.com'
});
const container = createContainer(provider);

document.body.appendChild(iframe);
```

## API reference

### handleFeature

```ts
container.handleFeature((params, { ok, err }) => {
if (params.tag === 'Chat') {
return ok(supportedChains.has(params.value));
}
return ok(false);
});
```

### handlePermissionRequest

```ts
container.handlePermissionRequest(async (params, { ok, err }) => {
if (params.tag === 'ChainConnect') {
// Show permission dialog to user
const approved = await showPermissionDialog(params.value);
return approved ? ok(undefined) : err({ tag: 'Rejected' });
}
return err({ tag: 'Unknown', value: { reason: 'Unsupported permission type' } });
});
```

### handleStorageRead

```ts
container.handleStorageRead(async (key, { ok, err }) => {
const value = await storage.get(key);
return ok(value ?? null);
});
```

### handleStorageWrite

```ts
container.handleStorageWrite(async ([key, value], { ok, err }) => {
try {
await storage.set(key, value);
return ok(undefined);
} catch (e) {
return err({ tag: 'Full' });
}
});
```

### handleStorageClear

```ts
container.handleStorageClear(async (key, { ok, err }) => {
await storage.delete(key);
return ok(undefined);
});
```

### handleAccountGet

```ts
container.handleAccountGet(async ([dotnsId, derivationIndex], { ok, err }) => {
const account = await getProductAccount(dotnsId, derivationIndex);
if (account) {
return ok({ publicKey: account.publicKey, name: account.name ?? null });
}
return err({ tag: 'NotConnected' });
});
```

### handleAccountGetAlias

```ts
container.handleAccountGetAlias(async ([dotnsId, derivationIndex], { ok, err }) => {
const alias = await getAccountAlias(dotnsId, derivationIndex);
if (alias) {
return ok({ context: alias.context, alias: alias.alias });
}
return err(new RequestCredentialsErr.NotConnected());
});
```

### handleAccountCreateProof

```ts
container.handleAccountCreateProof(async ([[dotnsId, derivationIndex], ringLocation, message], { ok, err }) => {
try {
const proof = await createRingProof(dotnsId, derivationIndex, ringLocation, message);
return ok(proof);
} catch (e) {
return err({ tag: 'RingNotFound' });
}
});
```

### handleGetLegacyAccounts

```ts
container.handleGetLegacyAccounts(async (_, { ok, err }) => {
const accounts = await getLegacyAccounts();
return ok(accounts);
});
```

### handleCreateTransaction

```ts
container.handleCreateTransaction(async ([productAccountId, payload], { ok, err }) => {
try {
const signedTx = await createTransaction(productAccountId, payload);
return ok(signedTx);
} catch (e) {
return err({ tag: 'Rejected' });
}
});
```

### handleCreateTransactionWithLegacyAccount

```ts
container.handleCreateTransactionWithLegacyAccount(async (payload, { ok, err }) => {
try {
const signedTx = await createTransactionWithLegacyAccount(payload);
return ok(signedTx);
} catch (e) {
return err({ tag: 'Rejected' });
}
});
```

### handleSignRaw

```ts
container.handleSignRaw(async (payload, { ok, err }) => {
try {
const result = await signRaw(payload);
return ok({ signature: result.signature, signedTransaction: result.signedTransaction });
} catch (e) {
return err({ tag: 'Rejected' });
}
});
```

### handleSignPayload

```ts
container.handleSignPayload(async (payload, { ok, err }) => {
try {
const result = await signPayload(payload);
return ok({ signature: result.signature, signedTransaction: result.signedTransaction ?? null });
} catch (e) {
return err({ tag: 'Rejected' });
}
});
```

### handleChatCreateContact

```ts
container.handleChatCreateContact(async (contact, { ok, err }) => {
await chatService.registerContact(contact);
return ok(undefined);
});
npm install @novasamatech/host-chat --save -E
```

### handleChatPostMessage
## Getting started

```ts
container.handleChatPostMessage(async (message, { ok, err }) => {
const messageId = await chatService.postMessage(message);
return ok({ messageId });
});
```

### handleChatActionSubscribe
import { createAccountService } from '@novasamatech/host-chat';
import { createLazyClient } from '@novasamatech/statement-store';

```ts
container.handleChatActionSubscribe((_, send, interrupt) => {
const listener = (action) => send(action);
chatService.on('action', listener);
return () => chatService.off('action', listener);
});
```
const lazyClient = createLazyClient(/* chain provider */);
const accounts = createAccountService('paseo-next-v2', lazyClient);

### handleStatementStoreCreateProof

```ts
container.handleStatementStoreCreateProof(async ([[dotnsId, derivationIndex], statement], { ok, err }) => {
try {
const proof = await createStatementProof(dotnsId, derivationIndex, statement);
return ok(proof);
} catch (e) {
return err({ tag: 'UnableToSign' });
// Search the off-chain username index for accounts whose username starts with `alice`.
const search = await accounts.search('alice', 'ASSIGNED');
if (search.isOk()) {
for (const hit of search.value) {
console.log(hit.candidateAccountId, hit.username);
}
});
```

### handleJsonRpcMessageSubscribe

```ts
import { getWsProvider } from 'polkadot-api/ws-provider';

const provider = getWsProvider('wss://rpc.polkadot.io');
container.handleJsonRpcMessageSubscribe(
{ genesisHash: '0x...' },
provider
);
```

### isReady
}

```ts
const ready = await container.isReady();
if (ready) {
console.log('Container is ready');
// Resolve a specific account's on-chain identity.
const identity = await accounts.getConsumerInfo('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
if (identity.isOk() && identity.value) {
console.log(identity.value.fullUsername, identity.value.credibility);
}
```

### dispose
### Networks

```ts
container.dispose();
```
`createAccountService` accepts one of:

### subscribeConnectionStatus
| Network | People chain endpoint |
| ---------------- | -------------------------------------- |
| `stable` | Polkadot People |
| `preview` | Westend People |
| `paseo-next` | Paseo People (V1) |
| `paseo-next-v2` | Paseo People (V2 multi-device) |

```ts
const unsubscribe = container.subscribeConnectionStatus((status) => {
console.log('Connection status:', status);
});
```
Each network entry pins both the People chain WebSocket URL (used via `lazyClient`) and
the off-chain identity-backend REST endpoint that `search` queries.

## PAPI provider support
## API

Host container supports [PAPI](https://papi.how/) request redirection from product to host container.
It can be useful to deduplicate socket connections or light client instances between multiple dapps.
### `createAccountService(network, lazyClient)`

To support this feature, you should add two additional handlers to the container:
Returns an object with two methods:

### Chain support check
```ts
const genesisHash = '0x...';
- **`search(query, status)`** — query the off-chain username index. `status` is
`'ASSIGNED' | 'PENDING'`. Resolves to a list of `{ candidateAccountId, username, status,
onchainData, createdAt, updatedAt }` rows.
- **`getConsumerInfo(address)`** — resolve a single SS58 address to an `Identity`
(`{ accountId, fullUsername, liteUsername, credibility }`) by reading
`Resources.Consumers` from the People chain. Returns `null` if the account has no
consumer entry. Tolerates both snake_case (V1) and camelCase (V2) runtime field names.

container.handleFeature(async (feature) => {
return feature.tag === 'Chain' && feature.value === genesisHash;
});
```
Both methods return `ResultAsync<…, Error>`; call `.isOk()` / `.isErr()` to discriminate.

## Codec subpath exports

### Provider implementation
The chat wire codecs are exposed under explicit subpaths so they can be tree-shaken
independently of the main entry point:

```ts
import { getWsProvider } from 'polkadot-api/ws-provider';
import {
ChatMessage,
TextContent,
RichTextContent,
ChatAcceptedContent,
DeviceAddedContent,
DeviceRemovedContent,
} from '@novasamatech/host-chat/codec/message';

const genesisHash = '0x...';
const provider = getWsProvider('wss://...');
import {
FileMeta,
FileVariant,
P2PMixnetFile,
} from '@novasamatech/host-chat/codec/attachment';

container.connectToPapiProvider(genesisHash, provider);
import type { ChatSession } from '@novasamatech/host-chat/session';
```

## Known pitfalls

### CSP error on iframe loading
If a dapp is hosted on a different domain than the container and uses HTTPS, you should add this meta tag to your host application HTML:

```html
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
```
These are byte-compatible with the Android / iOS Polkadot Mobile clients — modify with
care, the indices are pinned by the protocol.
1 change: 0 additions & 1 deletion packages/host-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "@novasamatech/host-chat",
"type": "module",
"version": "0.8.0-0",
"private": "true",
"description": "Host statement store chat integration",
"license": "Apache-2.0",
"repository": {
Expand Down
8 changes: 7 additions & 1 deletion packages/host-chat/src/accountService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type AccountService = {
getConsumerInfo(address: string): ResultAsync<Identity | null, Error>;
};

type Network = 'paseo-next' | 'preview' | 'stable';
type Network = 'paseo-next' | 'paseo-next-v2' | 'preview' | 'stable';

type SearchResponse = {
candidateAccountId: string;
Expand Down Expand Up @@ -146,4 +146,10 @@ const NETWORK_CONFIGS: Record<Network, NetworkConfig> = {
wsUrl: 'wss://paseo-people-next-rpc.polkadot.io',
apiUrl: 'https://identity-backend.parity-testnet.parity.io/api/v1',
},
'paseo-next-v2': {
id: 'paseo-next-v2',
name: 'Paseo Next V2',
wsUrl: 'wss://paseo-people-next-system-rpc.polkadot.io',
apiUrl: 'https://identity-backend-next.parity-testnet.parity.io/api/v1',
},
};