Skip to content

Commit 9435d77

Browse files
committed
docs: migration guide
1 parent 34b7ff4 commit 9435d77

2 files changed

Lines changed: 204 additions & 15 deletions

File tree

docs/migration/v0.8.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Migration Guide: v0.7 → v0.8
2+
3+
This guide covers all breaking changes and new APIs introduced in v0.8, organized by feature area.
4+
5+
## Overview of Breaking Changes
6+
7+
- [Multi-device SSO V2 wire protocol](#multi-device-sso-v2) — V1 SSO handshake is gone; both ends must run V2; persisted V1 sessions are dropped on first read
8+
- [`createPappAdapter` parameter change](#createpappadapter-parameter-change)`metadata: string` URL removed; host name / icon / platform now ride inline on `hostMetadata`
9+
- [`HostMetadata` reshape](#hostmetadata-reshape)`hostVersion / osType / osVersion``hostName / hostVersion / hostIcon / platformType / platformVersion / custom`
10+
- [`MessageContent.chatAccepted` payload](#messagecontentchataccepted-payload) — chat-accepted variant changed from `_void` to `{ messageId: string }`
11+
12+
---
13+
14+
## Breaking Changes
15+
16+
### Multi-device SSO V2
17+
18+
The SSO handshake has been replaced with the V2 multi-device protocol end-to-end. The consumer-facing surface on `pappAdapter.sso``pairingStatus`, `authenticate()`, `abortAuthentication()`, the `StoredUserSession` returned to consumers — is unchanged.
19+
20+
There is no V1 ↔ V2 compatibility:
21+
22+
- A host on v0.8 will not pair with a Polkadot Mobile build that only speaks V1.
23+
- A host on v0.7 will not pair with a multi-device Polkadot Mobile build.
24+
- Persisted v0.7 SSO session blobs decode short against the V2 codec and are silently dropped on first read — users with existing sessions need to re-pair.
25+
26+
Hosts and the paired Polkadot Mobile app must upgrade together.
27+
28+
---
29+
30+
### `createPappAdapter` parameter change
31+
32+
The `metadata: string` URL parameter (which pointed at a JSON file describing the host's `name` / `icon`) has been removed. Host identity now ships inline with the V2 QR proposal via the structured `hostMetadata` object, which also gains platform fields.
33+
34+
```ts
35+
// Before — v0.7
36+
createPappAdapter({
37+
appId: 'my-host',
38+
metadata: 'https://example.com/host-metadata.json',
39+
hostMetadata: {
40+
hostVersion: '1.2.3',
41+
osType: 'macOS',
42+
osVersion: '15.0',
43+
},
44+
});
45+
46+
// After — v0.8
47+
createPappAdapter({
48+
appId: 'my-host',
49+
hostMetadata: {
50+
hostName: 'My Host',
51+
hostVersion: '1.2.3',
52+
hostIcon: 'https://example.com/icon-256.png',
53+
platformType: 'macOS',
54+
platformVersion: '15.0',
55+
},
56+
});
57+
```
58+
59+
Hosts that previously served a `host-metadata.json` can drop the file — its contents now ride inline on `hostMetadata` as `hostName` and `hostIcon`.
60+
61+
---
62+
63+
### `HostMetadata` reshape
64+
65+
```ts
66+
// Before — v0.7
67+
type HostMetadata = {
68+
hostVersion?: string;
69+
osType?: string;
70+
osVersion?: string;
71+
};
72+
73+
// After — v0.8
74+
type HostMetadata = {
75+
hostName?: string;
76+
hostVersion?: string;
77+
hostIcon?: string;
78+
platformType?: string;
79+
platformVersion?: string;
80+
custom?: { name: string; value: string }[];
81+
};
82+
```
83+
84+
Mechanical renames:
85+
86+
| v0.7 | v0.8 |
87+
|-------------|-------------------|
88+
| `osType` | `platformType` |
89+
| `osVersion` | `platformVersion` |
90+
91+
The new optional fields — `hostName`, `hostIcon`, `custom` — replace what used to be fetched from the `metadata` URL and add a place for host-specific extension entries.
92+
93+
---
94+
95+
### `MessageContent.chatAccepted` payload
96+
97+
The legacy single-device `chatAccepted` variant used to be a `_void` marker. It now carries the originating chat-request message id so the recipient can correlate the accept with its request.
98+
99+
```ts
100+
// Before — v0.7
101+
{ tag: 'chatAccepted', value: undefined }
102+
103+
// After — v0.8
104+
{ tag: 'chatAccepted', value: { messageId: '...' } }
105+
```
106+
107+
Older clients that emit the v0.7 `_void` shape will fail to decode against the v0.8 codec.
108+
109+
The new multi-device accept lives at a separate index (`deviceChatAccepted`, index 20) — see [Multi-device chat message variants](#multi-device-chat-message-variants) below.
110+
111+
---
112+
113+
## New Features
114+
115+
### Auto-persisted device identity
116+
117+
The V2 protocol identifies the host by a per-installation device identity (sr25519 statement-account key + X25519 encryption key). The SDK now creates and persists this identity for you on first run, against the configured `StorageAdapter`, and reuses it on subsequent launches — no consumer wiring needed for web hosts.
118+
119+
For Electron / native hosts that want a hardware-backed identity (Keychain, native secure storage), pass an optional `deviceIdentity` factory:
120+
121+
```ts
122+
import type { DeviceIdentityForPairing } from '@novasamatech/host-papp';
123+
124+
createPappAdapter({
125+
appId: 'my-host',
126+
hostMetadata: { /* ... */ },
127+
async deviceIdentity(): Promise<DeviceIdentityForPairing> {
128+
return {
129+
statementAccountPublicKey,
130+
statementAccountSecret,
131+
encryptionPublicKey,
132+
encryptionPrivateKey,
133+
}
134+
},
135+
});
136+
```
137+
138+
The factory is invoked once per pairing flow.
139+
140+
### `onAuthSuccess` hook
141+
142+
A new optional caller hook fires after the V2 handshake succeeds — after the SDK has already written the session and secrets to its own repositories. Use it for consumer-specific post-pairing work (telemetry, custom peer caches, device-sync seeding).
143+
144+
```ts
145+
createPappAdapter({
146+
appId: 'my-host',
147+
hostMetadata: { /* ... */ },
148+
onAuthSuccess: ({ session, identityChatPrivateKey }) => {
149+
// session: StoredUserSession (already persisted by the SDK)
150+
// identityChatPrivateKey: the private half of session.identityChatPublicKey;
151+
// lives in UserSecretRepository and is surfaced here for consumers
152+
// whose chat / device-sync layer needs it directly.
153+
telemetry.track('sso_paired', { sessionId: session.id });
154+
},
155+
});
156+
```
157+
158+
Throwing from the hook fails the pending `authenticate()` call.
159+
160+
### New `StoredUserSession` V2 fields
161+
162+
`StoredUserSession` gains two optional V2 fields, populated when pairing happens against a V2 mobile build:
163+
164+
```ts
165+
type StoredUserSession = {
166+
// ... existing v0.7 fields ...
167+
identityAccountId?: AccountId; // user's identity sr25519 account
168+
identityChatPublicKey?: Uint8Array; // P-256 65-byte, derived locally from identityChatPrivateKey
169+
};
170+
```
171+
172+
Both are `Option`-wrapped on the wire so future schema revisions can append further fields without breaking decode of v0.8.0 blobs. The matching `identityChatPrivateKey` lives in `UserSecretRepository` and is surfaced through the [`onAuthSuccess`](#onauthsuccess-hook) hook.
173+
174+
### Multi-device chat message variants
175+
176+
`MessageContent` gains three new variants for the multi-device chat layer:
177+
178+
```ts
179+
// Index 17 — announce a new peer device on the chat
180+
{ tag: 'deviceAdded', value: { statementAccountId, encryptionPublicKey } }
181+
182+
// Index 18 — remove a peer device
183+
{ tag: 'deviceRemoved', value: { statementAccountId } }
184+
185+
// Index 20 — multi-device accept.
186+
// Sent on the identity-level session SessionId(B, A) encrypted with K(A, B)
187+
// so all of A's devices can decrypt without per-device envelope.
188+
{ tag: 'deviceChatAccepted', value: { requestId, device } }
189+
```
190+
191+
Index 19 is reserved as a spec placeholder so `deviceChatAccepted` lands on its specified index.

packages/host-papp/src/sso/auth/v2/proposal.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* subscription picks it up.
99
*/
1010

11+
import { enumValue } from '@novasamatech/scale';
1112
import { toHex } from 'polkadot-api/utils';
1213

1314
import { VersionedHandshakeProposal } from '../scale/handshakeV2.js';
@@ -25,7 +26,7 @@ export type HandshakeMetadata = {
2526
hostIcon?: string;
2627
platformType?: string;
2728
platformVersion?: string;
28-
custom?: { name: string; value: string }[];
29+
custom?: Record<string, string>;
2930
};
3031

3132
type MetadataEntryT = [
@@ -42,32 +43,29 @@ type MetadataEntryT = [
4243

4344
const buildMetadataEntries = (metadata: HandshakeMetadata): MetadataEntryT[] => {
4445
const entries: MetadataEntryT[] = [];
45-
if (metadata.hostName !== undefined) entries.push([{ tag: 'HostName', value: undefined }, metadata.hostName]);
46-
if (metadata.hostVersion !== undefined)
47-
entries.push([{ tag: 'HostVersion', value: undefined }, metadata.hostVersion]);
48-
if (metadata.hostIcon !== undefined) entries.push([{ tag: 'HostIcon', value: undefined }, metadata.hostIcon]);
49-
if (metadata.platformType !== undefined)
50-
entries.push([{ tag: 'PlatformType', value: undefined }, metadata.platformType]);
46+
if (metadata.hostName !== undefined) entries.push([enumValue('HostName', undefined), metadata.hostName]);
47+
if (metadata.hostVersion !== undefined) entries.push([enumValue('HostVersion', undefined), metadata.hostVersion]);
48+
if (metadata.hostIcon !== undefined) entries.push([enumValue('HostIcon', undefined), metadata.hostIcon]);
49+
if (metadata.platformType !== undefined) entries.push([enumValue('PlatformType', undefined), metadata.platformType]);
5150
if (metadata.platformVersion !== undefined) {
52-
entries.push([{ tag: 'PlatformVersion', value: undefined }, metadata.platformVersion]);
51+
entries.push([enumValue('PlatformVersion', undefined), metadata.platformVersion]);
5352
}
54-
for (const c of metadata.custom ?? []) {
55-
entries.push([{ tag: 'Custom', value: c.name }, c.value]);
53+
for (const [key, value] of Object.entries(metadata.custom ?? {})) {
54+
entries.push([enumValue('Custom', key), value]);
5655
}
5756
return entries;
5857
};
5958

6059
export const encodeProposal = (device: HandshakeProposalDevice, metadata: HandshakeMetadata): Uint8Array =>
61-
VersionedHandshakeProposal.enc({
62-
tag: 'V2',
63-
value: {
60+
VersionedHandshakeProposal.enc(
61+
enumValue('V2', {
6462
device: {
6563
statementAccountId: device.statementAccountPublicKey,
6664
encryptionPublicKey: device.encryptionPublicKey,
6765
},
6866
metadata: buildMetadataEntries(metadata),
69-
},
70-
});
67+
}),
68+
);
7169

7270
export const buildPairingDeeplink = (device: HandshakeProposalDevice, metadata: HandshakeMetadata): string => {
7371
const bytes = encodeProposal(device, metadata);

0 commit comments

Comments
 (0)