@novasamatech/host-papp 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.papi/descriptors/dist/common-types.d.ts +1483 -1359
- package/.papi/descriptors/dist/descriptors-BLqmZ_vS.js +25 -0
- package/.papi/descriptors/dist/index.d.ts +1 -1
- package/.papi/descriptors/dist/index.js +8 -6
- package/.papi/descriptors/dist/metadataTypes-wXgvb64i.js +3 -0
- package/.papi/descriptors/dist/metadataTypes.d.ts +1 -1
- package/.papi/descriptors/dist/people_lite.d.ts +1175 -1001
- package/.papi/descriptors/dist/people_lite_metadata-CwU9OBGy.js +3 -0
- package/.papi/descriptors/generated.json +1 -1
- package/.papi/descriptors/package.json +1 -1
- package/.papi/metadata/people_lite.scale +0 -0
- package/.papi/polkadot-api.json +3 -3
- package/README.md +140 -87
- package/dist/identity/identifierKey.d.ts +19 -0
- package/dist/identity/identifierKey.js +17 -0
- package/dist/identity/impl.js +12 -17
- package/dist/identity/index.d.ts +2 -0
- package/dist/identity/index.js +1 -0
- package/dist/identity/rpcAdapter.d.ts +5 -1
- package/dist/identity/rpcAdapter.js +9 -3
- package/dist/identity/types.d.ts +9 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/sso/sessionManager/scale/remoteMessage.d.ts +102 -2
- package/dist/sso/sessionManager/scale/remoteMessage.js +11 -1
- package/dist/sso/sessionManager/scale/resourceAllocation.d.ts +3 -0
- package/dist/sso/sessionManager/scale/resourceAllocation.js +11 -0
- package/dist/sso/sessionManager/scale/ringVrf.d.ts +43 -2
- package/dist/sso/sessionManager/scale/ringVrf.js +12 -1
- package/dist/sso/sessionManager/scale/ringVrfKeys.d.ts +85 -0
- package/dist/sso/sessionManager/scale/ringVrfKeys.js +22 -0
- package/dist/sso/sessionManager/userSession.d.ts +22 -3
- package/dist/sso/sessionManager/userSession.js +59 -3
- package/package.json +9 -5
- package/.papi/descriptors/dist/descriptors-D5oN4i7I.js +0 -25
- package/.papi/descriptors/dist/metadataTypes-BaUy_4gO.js +0 -3
- package/.papi/descriptors/dist/people_lite_metadata-Cj1JLFI2.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"cliVersion":"0.21.
|
|
1
|
+
{"cliVersion":"0.21.9","whitelist":null,"chains":{"people_lite":"0xcca79c4482b2233e7cd4d24a23988e77"},"contracts":{"ink":{},"sol":{}}}
|
|
Binary file
|
package/.papi/polkadot-api.json
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
},
|
|
7
7
|
"entries": {
|
|
8
8
|
"people_lite": {
|
|
9
|
-
"wsUrl": "wss://
|
|
9
|
+
"wsUrl": "wss://people-paseo.gatotech.network",
|
|
10
10
|
"metadata": ".papi/metadata/people_lite.scale",
|
|
11
|
-
"genesis": "
|
|
12
|
-
"codeHash": "
|
|
11
|
+
"genesis": "0xe6c30d6e148f250b887105237bcaa5cb9f16dd203bf7b5b9d4f1da7387cb86ec",
|
|
12
|
+
"codeHash": "0x82a343ddb8c1faf6d88721f6188dabb393b3d5b4fd54aae6d621bc3b0452ebbb"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
package/README.md
CHANGED
|
@@ -4,17 +4,16 @@ Polkadot app integration layer for host applications.
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`@novasamatech/host-papp` is the integration SDK that lets a javascript-based host embed Polkadot
|
|
8
|
-
|
|
7
|
+
`@novasamatech/host-papp` is the integration SDK that lets a javascript-based host embed Polkadot Mobile capabilities.
|
|
8
|
+
It encapsulates everything needed to:
|
|
9
9
|
|
|
10
10
|
- pair the host with a Polkadot wallet/SSO provider via a deeplink handshake;
|
|
11
11
|
- store and manage paired user sessions;
|
|
12
12
|
- send signing requests and ring-VRF alias requests to the paired wallet;
|
|
13
13
|
- look up on-chain identity information for accounts.
|
|
14
14
|
|
|
15
|
-
The package is UI-framework agnostic — it exposes plain async APIs and observable state
|
|
16
|
-
|
|
17
|
-
non-browser runtime.
|
|
15
|
+
The package is UI-framework agnostic — it exposes plain async APIs and observable state (`subscribe` / `read`), so it
|
|
16
|
+
can be wired into React, Vue, Svelte, vanilla DOM, or a non-browser runtime.
|
|
18
17
|
|
|
19
18
|
## Installation
|
|
20
19
|
|
|
@@ -24,8 +23,7 @@ npm install @novasamatech/host-papp --save -E
|
|
|
24
23
|
|
|
25
24
|
## Getting started
|
|
26
25
|
|
|
27
|
-
Create a single adapter instance for the lifetime of your host app and share it across the
|
|
28
|
-
features that need it.
|
|
26
|
+
Create a single adapter instance for the lifetime of your host app and share it across the features that need it.
|
|
29
27
|
|
|
30
28
|
```ts
|
|
31
29
|
import { createPappAdapter } from '@novasamatech/host-papp';
|
|
@@ -46,26 +44,25 @@ const papp = createPappAdapter({
|
|
|
46
44
|
|
|
47
45
|
`createPappAdapter` returns five sub-modules:
|
|
48
46
|
|
|
49
|
-
| Module | Purpose
|
|
50
|
-
| ---------------- |
|
|
51
|
-
| `papp.sso` | Authentication / pairing flow with a remote wallet.
|
|
52
|
-
| `papp.sessions` | List of paired user sessions and per-session messaging (sign, etc.)
|
|
53
|
-
| `papp.secrets` | Local secret storage for the derived guest accounts.
|
|
54
|
-
| `papp.identity` | On-chain identity lookups for arbitrary account ids.
|
|
55
|
-
| `papp.allowance` | Resource allowances (bulletin / statement-store signers) per product
|
|
47
|
+
| Module | Purpose |
|
|
48
|
+
| ---------------- | --------------------------------------------------------------------- |
|
|
49
|
+
| `papp.sso` | Authentication / pairing flow with a remote wallet. |
|
|
50
|
+
| `papp.sessions` | List of paired user sessions and per-session messaging (sign, etc.). |
|
|
51
|
+
| `papp.secrets` | Local secret storage for the derived guest accounts. |
|
|
52
|
+
| `papp.identity` | On-chain identity lookups for arbitrary account ids. |
|
|
53
|
+
| `papp.allowance` | Resource allowances (bulletin / statement-store signers) per product. |
|
|
56
54
|
|
|
57
|
-
Custom adapters (statement store, identity RPC, storage, lazy chain client) can be supplied
|
|
58
|
-
|
|
55
|
+
Custom adapters (statement store, identity RPC, storage, lazy chain client) can be supplied via the `adapters` option
|
|
56
|
+
for testing or non-browser environments.
|
|
59
57
|
|
|
60
58
|
## Authentication and pairing (V1)
|
|
61
59
|
|
|
62
|
-
The V1 SSO flow described in this section is the single-device pairing protocol used by
|
|
63
|
-
|
|
64
|
-
[V2 SSO handshake](#v2-sso-handshake) below.
|
|
60
|
+
The V1 SSO flow described in this section is the single-device pairing protocol used by `papp.sso.authenticate()`. For
|
|
61
|
+
the multi-device V2 protocol see [V2 SSO handshake](#v2-sso-handshake) below.
|
|
65
62
|
|
|
66
|
-
`papp.sso.authenticate()` runs the full pairing + attestation flow and resolves with the
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
`papp.sso.authenticate()` runs the full pairing + attestation flow and resolves with the stored user session, or `null`
|
|
64
|
+
if the flow was aborted. The flow is idempotent — calling it again while a previous run is in flight returns the same
|
|
65
|
+
in-progress promise.
|
|
69
66
|
|
|
70
67
|
```ts
|
|
71
68
|
const result = await papp.sso.authenticate();
|
|
@@ -92,8 +89,8 @@ papp.sso.abortAuthentication();
|
|
|
92
89
|
|
|
93
90
|
### Reacting to pairing status
|
|
94
91
|
|
|
95
|
-
The pairing process is observable. UI code typically renders a QR code / deeplink while the
|
|
96
|
-
|
|
92
|
+
The pairing process is observable. UI code typically renders a QR code / deeplink while the status is `pairing`, then
|
|
93
|
+
transitions to a "signing in" screen during attestation.
|
|
97
94
|
|
|
98
95
|
```ts
|
|
99
96
|
import type { PairingStatus } from '@novasamatech/host-papp';
|
|
@@ -121,15 +118,14 @@ render(papp.sso.pairingStatus.read());
|
|
|
121
118
|
const unsubscribe = papp.sso.pairingStatus.subscribe(render);
|
|
122
119
|
```
|
|
123
120
|
|
|
124
|
-
`papp.sso.attestationStatus` exposes the same `read` / `subscribe` shape and tracks
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
steps before `attestation`, then attestation, then back to pairing's `finished`.
|
|
121
|
+
`papp.sso.attestationStatus` exposes the same `read` / `subscribe` shape and tracks attestation progress (`attestation`
|
|
122
|
+
with a claimed `username`, `attestationError`, or `finished`). For convenience, treat the two streams as a single
|
|
123
|
+
derived UI state — pairing steps before `attestation`, then attestation, then back to pairing's `finished`.
|
|
128
124
|
|
|
129
125
|
## Managing user sessions
|
|
130
126
|
|
|
131
|
-
`papp.sessions.sessions` is an observable list of currently paired sessions. Most host apps
|
|
132
|
-
|
|
127
|
+
`papp.sessions.sessions` is an observable list of currently paired sessions. Most host apps work with the first one
|
|
128
|
+
(single-user model), but the SDK does not enforce that.
|
|
133
129
|
|
|
134
130
|
```ts
|
|
135
131
|
import type { UserSession } from '@novasamatech/host-papp';
|
|
@@ -144,8 +140,7 @@ const unsubscribe = papp.sessions.sessions.subscribe(sessions => {
|
|
|
144
140
|
currentSession = papp.sessions.sessions.read().at(0) ?? null;
|
|
145
141
|
```
|
|
146
142
|
|
|
147
|
-
Disconnecting notifies the wallet, removes local secrets, and triggers the subscription
|
|
148
|
-
above.
|
|
143
|
+
Disconnecting notifies the wallet, removes local secrets, and triggers the subscription above.
|
|
149
144
|
|
|
150
145
|
```ts
|
|
151
146
|
const disconnect = async (session: UserSession) => {
|
|
@@ -159,8 +154,7 @@ const disconnect = async (session: UserSession) => {
|
|
|
159
154
|
|
|
160
155
|
## Signing
|
|
161
156
|
|
|
162
|
-
A `UserSession` exposes `signPayload` and `signRaw` for forwarding signing requests to the
|
|
163
|
-
paired wallet.
|
|
157
|
+
A `UserSession` exposes `signPayload` and `signRaw` for forwarding signing requests to the paired wallet.
|
|
164
158
|
|
|
165
159
|
```ts
|
|
166
160
|
const signed = await currentSession.signPayload({
|
|
@@ -197,10 +191,9 @@ await currentSession.signRaw({
|
|
|
197
191
|
});
|
|
198
192
|
```
|
|
199
193
|
|
|
200
|
-
`signVrf` asks the wallet for an sr25519 (schnorrkel) VRF signature from a product account
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
transcript and signs. Callers that need a `signer` item must supply their own public key;
|
|
194
|
+
`signVrf` asks the wallet for an sr25519 (schnorrkel) VRF signature from a product account (RFC-0023). The transcript
|
|
195
|
+
travels as a recipe — a root domain-separation label plus an ordered list of `(label, value)` items — which the wallet
|
|
196
|
+
replays verbatim into a Merlin transcript and signs. Callers that need a `signer` item must supply their own public key;
|
|
204
197
|
the host never injects it.
|
|
205
198
|
|
|
206
199
|
```ts
|
|
@@ -222,44 +215,82 @@ vrf.match(
|
|
|
222
215
|
);
|
|
223
216
|
```
|
|
224
217
|
|
|
225
|
-
This is the non-`AutoSigning` path only: when `AutoSigning` covers the account the host
|
|
226
|
-
|
|
218
|
+
This is the non-`AutoSigning` path only: when `AutoSigning` covers the account the host signs locally and never
|
|
219
|
+
round-trips to the wallet.
|
|
227
220
|
|
|
228
|
-
## Ring VRF proofs and aliases
|
|
221
|
+
## Ring VRF keys, proofs and aliases
|
|
229
222
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
223
|
+
Ring VRF member keys are **explicit and product-owned** (RFC-0024): a product registers the keys it owns against the
|
|
224
|
+
rings it intends them for, other products discover those registrations by an anonymized handle, and the handle is passed
|
|
225
|
+
to every call that uses the key. The paired device is the authoritative registry — it needs the complete set to serve
|
|
226
|
+
slot assignment and PGAS claims, and to show the user what their keys are used for.
|
|
234
227
|
|
|
235
228
|
```ts
|
|
236
|
-
// [productId, suffix]. The suffix is the wire `Index(u32) | Raw([u8; 32])` selector
|
|
237
|
-
// (RFC 0022): `Index` for a plain index, `Raw` for a raw 32-byte index. It
|
|
238
|
-
// expands to the same 32-byte value as a product account's derivation index.
|
|
239
|
-
const context = ['product.dot', { tag: 'Index', value: 0 }];
|
|
240
229
|
const ring = {
|
|
241
230
|
chainId: '0x…', // 32-byte chain genesis hash
|
|
242
231
|
junctions: [{ tag: 'PalletInstance', value: 42 }],
|
|
243
232
|
};
|
|
244
233
|
|
|
245
|
-
|
|
234
|
+
// Register a key owned by `peopl.dot` at index 0 for that ring. Consent-free and
|
|
235
|
+
// idempotent — re-registering an index for another ring extends the entry.
|
|
236
|
+
const publicKey = await currentSession.registerRingVrfKey('peopl.dot', { tag: 'Index', value: 0 }, ring);
|
|
237
|
+
|
|
238
|
+
// Discover another product's keys. `'PublicKey'` disclosure additionally returns
|
|
239
|
+
// the member public key, which is linkable across every ring it appears in and so
|
|
240
|
+
// is permissioned cross-product.
|
|
241
|
+
const entries = await currentSession.listRingVrfKeys('game.dot', 'peopl.dot', 'Anonymized');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A `UserSession` can then ask the paired device for a privacy-preserving contextual alias, or a ring VRF proof, for a
|
|
245
|
+
given `keyHandle`, product-scoped `context` and `ring` location. `callingProductId` names the product the host is acting
|
|
246
|
+
for — it is what the owner's allowlist is checked against when the handle is foreign. Both take the same
|
|
247
|
+
`(keyHandle, context, ring)` so the alias in the proof matches `getRingVrfAlias`.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// The key handle names a slot in the owner's ring VRF domain. Select it from
|
|
251
|
+
// `listRingVrfKeys` by declared ring and pass it through opaquely — never
|
|
252
|
+
// hardcode another product's index.
|
|
253
|
+
const keyHandle = ['peopl.dot', { tag: 'Index', value: 0 }];
|
|
254
|
+
|
|
255
|
+
// [productId, suffix]. The suffix is the wire `Index(u32) | Raw([u8; 32])` selector
|
|
256
|
+
// (RFC 0022): `Index` for a plain index, `Raw` for a raw 32-byte index. It
|
|
257
|
+
// expands to the same 32-byte value as a product account's derivation index.
|
|
258
|
+
const context = ['product.dot', { tag: 'Index', value: 0 }];
|
|
259
|
+
|
|
260
|
+
const alias = await currentSession.getRingVrfAlias('caller.dot', keyHandle, context, ring);
|
|
246
261
|
|
|
247
|
-
const proof = await currentSession.createRingVrfProof(
|
|
262
|
+
const proof = await currentSession.createRingVrfProof(
|
|
263
|
+
'caller.dot',
|
|
264
|
+
keyHandle,
|
|
265
|
+
context,
|
|
266
|
+
ring,
|
|
267
|
+
new Uint8Array([0x48, 0x69]),
|
|
268
|
+
);
|
|
248
269
|
proof.match(
|
|
249
270
|
({ proof, contextualAlias, ringIndex, ringRevision }) =>
|
|
250
271
|
console.log('proof at ring', ringIndex, 'revision', ringRevision),
|
|
251
|
-
// failures decode to a structured `
|
|
272
|
+
// failures decode to a structured `CreateProofErr` — RingNotFound / NotMember /
|
|
273
|
+
// KeyNotRegistered / KeyNotInRing / NotAllowlisted / Rejected / Unknown
|
|
252
274
|
error => console.error('proof failed:', error),
|
|
253
275
|
);
|
|
276
|
+
|
|
277
|
+
// `ringVrfSign` signs with the member key itself instead of proving membership
|
|
278
|
+
// anonymously. No context and no ring — nothing for either to scope — and the
|
|
279
|
+
// result is verified against the member public key, so it is linkable to every
|
|
280
|
+
// other use of that key.
|
|
281
|
+
const signature = await currentSession.ringVrfSign('caller.dot', keyHandle, new Uint8Array([0x48, 0x69]));
|
|
254
282
|
```
|
|
255
283
|
|
|
284
|
+
Producing a proof or a signature with a **foreign** key handle is gated on the key's owning product having allowlisted
|
|
285
|
+
the caller in its manifest, and there is deliberately no user-prompt fallback: `message` is opaque, so consenting to it
|
|
286
|
+
is not meaningful consent, and only the owner is positioned to evaluate the risk. Reading an alias authorizes nothing
|
|
287
|
+
and stays on the ordinary grant-or-prompt path.
|
|
288
|
+
|
|
256
289
|
## Product subtree public keys
|
|
257
290
|
|
|
258
|
-
Product accounts live at `//product//{productId}/{index}` (RFC 0022). The
|
|
259
|
-
product
|
|
260
|
-
|
|
261
|
-
product-subtree public key once, then soft-derives account public keys locally
|
|
262
|
-
from it.
|
|
291
|
+
Product accounts live at `//product//{productId}/{index}` (RFC 0022). The product junction is **hard**, so the user's
|
|
292
|
+
root public key alone no longer determines product account public keys — the host asks the paired device for the
|
|
293
|
+
product-subtree public key once, then soft-derives account public keys locally from it.
|
|
263
294
|
|
|
264
295
|
```ts
|
|
265
296
|
const subtreeKey = await currentSession.getProductSubtree('product.dot');
|
|
@@ -270,15 +301,21 @@ subtreeKey.match(
|
|
|
270
301
|
);
|
|
271
302
|
```
|
|
272
303
|
|
|
273
|
-
The request is consent-free — the response carries no secret material. Only
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
304
|
+
The request is consent-free — the response carries no secret material. Only `AutoSigning` does: its payload is the
|
|
305
|
+
product-subtree secret key (`productRootPrivateKey`, 64-byte expanded sr25519 secret), which exposes exactly that
|
|
306
|
+
product's subtree, plus `ringVrfDomainEntropy` (RFC-0024) — the entropy of the `//{productId}` node of the disjoint ring
|
|
307
|
+
VRF tree, which lets the host derive the member secret of a **registered** key locally. The former
|
|
308
|
+
`productDerivationSecret` is gone.
|
|
309
|
+
|
|
310
|
+
Bundling the entropy widens the grant: "sign transactions without prompting me" and "produce personhood proofs offline"
|
|
311
|
+
become one decision. And because derivation from the entropy is unconditional arithmetic, the registry is what
|
|
312
|
+
distinguishes a meaningful index from a meaningless one — a host MUST NOT derive a member secret for a
|
|
313
|
+
`(product, index)` pair absent from its registry.
|
|
277
314
|
|
|
278
315
|
## Identity lookups
|
|
279
316
|
|
|
280
|
-
`papp.identity` resolves on-chain identity data (lite / full username, credibility,
|
|
281
|
-
|
|
317
|
+
`papp.identity` resolves on-chain identity data (lite / full username, credibility, chat encryption key) for arbitrary
|
|
318
|
+
account ids. Pass an `0x`-prefixed account id (32-byte hex).
|
|
282
319
|
|
|
283
320
|
```ts
|
|
284
321
|
const lookup = async (accountId: string) => {
|
|
@@ -296,34 +333,51 @@ const lookup = async (accountId: string) => {
|
|
|
296
333
|
await papp.identity.getIdentities([accountIdA, accountIdB]);
|
|
297
334
|
```
|
|
298
335
|
|
|
299
|
-
A paired `UserSession` also exposes `getIdentity()` as a shortcut that looks up the identity
|
|
300
|
-
|
|
336
|
+
A paired `UserSession` also exposes `getIdentity()` as a shortcut that looks up the identity of its own user identity
|
|
337
|
+
account — no account id to pass:
|
|
301
338
|
|
|
302
339
|
```ts
|
|
303
340
|
const identity = await session.getIdentity(); // Result<Identity | null, Error>
|
|
304
341
|
```
|
|
305
342
|
|
|
343
|
+
`Identity` is:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
type Identity = {
|
|
347
|
+
accountId: string;
|
|
348
|
+
fullUsername: string | null;
|
|
349
|
+
liteUsername: string;
|
|
350
|
+
credibility: { type: 'Lite' } | { type: 'Person'; alias: `0x${string}`; lastUpdate: string | null };
|
|
351
|
+
/** 32-byte X25519 chat encryption key as hex, unwrapped from the 65-byte
|
|
352
|
+
* RFC-0004 container; null for a keypair type this SDK does not implement. */
|
|
353
|
+
identifierKey: `0x${string}` | null;
|
|
354
|
+
};
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`lastUpdate` and `identifierKey` are both nullable — the chain record can carry neither a readable timestamp nor a key
|
|
358
|
+
this SDK can use, and neither case is an error.
|
|
359
|
+
|
|
360
|
+
This package owns the `Resources.Consumers` reader for the monorepo. `host-chat` reads through it rather than querying
|
|
361
|
+
the chain itself; `createIdentityRpcAdapter` is exported for that, alongside the `Identity` and `Credibility` types.
|
|
362
|
+
|
|
306
363
|
## V2 SSO handshake
|
|
307
364
|
|
|
308
|
-
V2 is a redesign of the SSO pairing flow that supports the same user identity across
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
identity, so contacts, chats, and roster events are shared between them.
|
|
365
|
+
V2 is a redesign of the SSO pairing flow that supports the same user identity across multiple devices. The host
|
|
366
|
+
generates a stable device keypair locally, emits a `VersionedHandshakeProposal::V2` via QR/deeplink, and an authorising
|
|
367
|
+
peer (e.g. the user's existing Polkadot App) responds over the Statement Store with the user identity keys signed to
|
|
368
|
+
authorise this device. Subsequent devices belonging to the same user reuse the same identity, so contacts, chats, and
|
|
369
|
+
roster events are shared between them.
|
|
314
370
|
|
|
315
|
-
V2 is **not interoperable with V1**: a V1-only peer can't decode a V2 proposal QR and vice
|
|
316
|
-
|
|
371
|
+
V2 is **not interoperable with V1**: a V1-only peer can't decode a V2 proposal QR and vice versa. Hosts that want to
|
|
372
|
+
support both should branch on which protocol the peer advertises.
|
|
317
373
|
|
|
318
374
|
### The flow
|
|
319
375
|
|
|
320
376
|
1. The host builds a pairing deeplink from its device keypair and shows it as a QR code.
|
|
321
|
-
2. The authorising device scans it and posts its response to the Statement Store: first a
|
|
322
|
-
|
|
323
|
-
signed to authorise this device — or `Failed`.
|
|
377
|
+
2. The authorising device scans it and posts its response to the Statement Store: first a `Pending` acknowledgement,
|
|
378
|
+
then either `Success` — carrying the user's identity keys, signed to authorise this device — or `Failed`.
|
|
324
379
|
3. The host polls the pairing topic, decrypts and verifies each response, and drives a
|
|
325
|
-
`Submitted → Pending → Success | Failed` state machine. On `Success` it persists the
|
|
326
|
-
user identity.
|
|
380
|
+
`Submitted → Pending → Success | Failed` state machine. On `Success` it persists the user identity.
|
|
327
381
|
|
|
328
382
|
### Building and rendering the QR
|
|
329
383
|
|
|
@@ -333,7 +387,7 @@ import { buildPairingDeeplink } from '@novasamatech/host-papp';
|
|
|
333
387
|
const deeplink = buildPairingDeeplink(
|
|
334
388
|
{
|
|
335
389
|
statementAccountPublicKey: device.statementAccountPublicKey, // sr25519 device pubkey, 32 bytes
|
|
336
|
-
encryptionPublicKey: device.encryptionPublicKey,
|
|
390
|
+
encryptionPublicKey: device.encryptionPublicKey, // P-256 device pubkey, 65 bytes uncompressed
|
|
337
391
|
},
|
|
338
392
|
{
|
|
339
393
|
hostName: 'My Host App',
|
|
@@ -396,9 +450,9 @@ pairing.abort();
|
|
|
396
450
|
|
|
397
451
|
### Surviving reloads / proper logout
|
|
398
452
|
|
|
399
|
-
The chain holds the most recent statement on the pairing topic indefinitely, so on cold
|
|
400
|
-
|
|
401
|
-
|
|
453
|
+
The chain holds the most recent statement on the pairing topic indefinitely, so on cold start the service will see the
|
|
454
|
+
previous Success and replay it. To distinguish a stale replay from a fresh re-pair, callers can pass byte-level dedupe
|
|
455
|
+
state:
|
|
402
456
|
|
|
403
457
|
```ts
|
|
404
458
|
const pairing = startPairingV2({
|
|
@@ -410,14 +464,13 @@ const pairing = startPairingV2({
|
|
|
410
464
|
});
|
|
411
465
|
```
|
|
412
466
|
|
|
413
|
-
The service skips any incoming statement whose bytes match `initialProcessedDataHex`. PApp
|
|
414
|
-
|
|
415
|
-
always produces different bytes and passes the dedupe.
|
|
467
|
+
The service skips any incoming statement whose bytes match `initialProcessedDataHex`. PApp re-encrypts every Success
|
|
468
|
+
with a fresh ephemeral key + AES-GCM nonce, so a genuine re-pair always produces different bytes and passes the dedupe.
|
|
416
469
|
|
|
417
470
|
## Reading allowances
|
|
418
471
|
|
|
419
|
-
Each `UserSession` can read its own persisted allowance slot-account key for a given
|
|
420
|
-
|
|
472
|
+
Each `UserSession` can read its own persisted allowance slot-account key for a given product and resource. The session
|
|
473
|
+
id is implicit — you only pass the product and resource:
|
|
421
474
|
|
|
422
475
|
```ts
|
|
423
476
|
const session = papp.sessions.sessions.read().at(0);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Resources.Consumers.identifier_key` — a peer's chat encryption key as the chain
|
|
3
|
+
* records it (CHAT-RFC-0004 §4).
|
|
4
|
+
*
|
|
5
|
+
* The 65-byte width predates X25519: it is what an uncompressed P-256 point occupied,
|
|
6
|
+
* and it stayed when the curve changed. Only the keypair type and the key width moved
|
|
7
|
+
* (0x04 + 64 → 0x00 + 32), so the field is still `SizedHex<65>` in runtime metadata.
|
|
8
|
+
*
|
|
9
|
+
* Padding is carried as a field because the RFC requires readers to ignore it rather
|
|
10
|
+
* than validate it. Decoding throws on a keypair type this SDK does not implement;
|
|
11
|
+
* `decodeRawIdentity` maps that to `null`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const IdentifierKey: import("scale-ts").Codec<{
|
|
14
|
+
tag: "X25519";
|
|
15
|
+
value: {
|
|
16
|
+
key: Uint8Array<ArrayBufferLike>;
|
|
17
|
+
padding: Uint8Array<ArrayBufferLike>;
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Bytes, Enum } from '@novasamatech/scale';
|
|
2
|
+
import { Struct } from 'scale-ts';
|
|
3
|
+
/**
|
|
4
|
+
* `Resources.Consumers.identifier_key` — a peer's chat encryption key as the chain
|
|
5
|
+
* records it (CHAT-RFC-0004 §4).
|
|
6
|
+
*
|
|
7
|
+
* The 65-byte width predates X25519: it is what an uncompressed P-256 point occupied,
|
|
8
|
+
* and it stayed when the curve changed. Only the keypair type and the key width moved
|
|
9
|
+
* (0x04 + 64 → 0x00 + 32), so the field is still `SizedHex<65>` in runtime metadata.
|
|
10
|
+
*
|
|
11
|
+
* Padding is carried as a field because the RFC requires readers to ignore it rather
|
|
12
|
+
* than validate it. Decoding throws on a keypair type this SDK does not implement;
|
|
13
|
+
* `decodeRawIdentity` maps that to `null`.
|
|
14
|
+
*/
|
|
15
|
+
export const IdentifierKey = Enum({
|
|
16
|
+
X25519: Struct({ key: Bytes(32), padding: Bytes(32) }),
|
|
17
|
+
});
|
package/dist/identity/impl.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResultAsync, okAsync } from 'neverthrow';
|
|
2
2
|
import { defer, distinctUntilChanged, filter, finalize, map, merge, shareReplay, takeUntil, tap, timer } from 'rxjs';
|
|
3
|
-
import { toError } from '../helpers/utils.js';
|
|
4
3
|
const WATCH_IDENTITY_INITIAL_TIMEOUT_MS = 15_000;
|
|
5
4
|
function getCacheKey(accountId) {
|
|
6
5
|
return `identity_${accountId}`;
|
|
7
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* An older-shape record reads as absent so the caller refetches — `getIdentity` only goes
|
|
9
|
+
* to chain on a `null` hit. Extend whenever `Identity` grows a field.
|
|
10
|
+
*/
|
|
11
|
+
const REQUIRED_FIELDS = ['accountId', 'fullUsername', 'liteUsername', 'credibility', 'identifierKey'];
|
|
8
12
|
function parseIdentity(raw) {
|
|
9
13
|
if (!raw)
|
|
10
14
|
return null;
|
|
11
15
|
try {
|
|
12
|
-
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return REQUIRED_FIELDS.every(field => field in parsed) ? parsed : null;
|
|
13
18
|
}
|
|
14
19
|
catch {
|
|
15
20
|
return null;
|
|
@@ -28,7 +33,7 @@ function readCachedIdentity(storage, accountId) {
|
|
|
28
33
|
return defer(() => storage.read(getCacheKey(accountId)).match(parseIdentity, () => null));
|
|
29
34
|
}
|
|
30
35
|
export function createIdentityRepository({ adapter, storage, initialEmissionTimeoutMs = WATCH_IDENTITY_INITIAL_TIMEOUT_MS, }) {
|
|
31
|
-
const cachedRequester = createCachedIdentityRequester(storage
|
|
36
|
+
const cachedRequester = createCachedIdentityRequester(storage);
|
|
32
37
|
// Per-account de-dup: concurrent watchIdentity(acc) calls share one chain
|
|
33
38
|
// subscription via the shared stream built below. The entry clears itself —
|
|
34
39
|
// see the `finalize` in `buildWatch` — so the map can't accumulate dead
|
|
@@ -81,25 +86,15 @@ export function createIdentityRepository({ adapter, storage, initialEmissionTime
|
|
|
81
86
|
},
|
|
82
87
|
};
|
|
83
88
|
}
|
|
84
|
-
function createCachedIdentityRequester(storage
|
|
89
|
+
function createCachedIdentityRequester(storage) {
|
|
85
90
|
function readSingleCacheRecord(accountId) {
|
|
86
|
-
return storage.read(
|
|
87
|
-
if (!raw) {
|
|
88
|
-
return ok(null);
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
return ok(JSON.parse(raw));
|
|
92
|
-
}
|
|
93
|
-
catch (e) {
|
|
94
|
-
return err(toError(e));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
91
|
+
return storage.read(getCacheKey(accountId)).map(parseIdentity);
|
|
97
92
|
}
|
|
98
93
|
function writeSingleCacheRecord(accountId, identity) {
|
|
99
94
|
if (identity === null) {
|
|
100
95
|
return okAsync(undefined);
|
|
101
96
|
}
|
|
102
|
-
return storage.write(
|
|
97
|
+
return storage.write(getCacheKey(accountId), JSON.stringify(identity));
|
|
103
98
|
}
|
|
104
99
|
function readCache(accounts) {
|
|
105
100
|
if (accounts.length === 0) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createIdentityRpcAdapter } from './rpcAdapter.js';
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { LazyClient } from '@novasamatech/statement-store';
|
|
2
|
-
import type {
|
|
2
|
+
import type { People_liteQueries } from '../../.papi/descriptors/dist/index.js';
|
|
3
|
+
import type { Identity, IdentityAdapter } from './types.js';
|
|
4
|
+
type RawConsumers = NonNullable<People_liteQueries['Resources']['Consumers']['Value']>;
|
|
5
|
+
export declare function decodeRawIdentity(accountId: string, raw: RawConsumers | undefined, textDecoder: TextDecoder): Identity | null;
|
|
3
6
|
export declare function createIdentityRpcAdapter(lazyClient: LazyClient): IdentityAdapter;
|
|
7
|
+
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toHex } from '@novasamatech/scale';
|
|
2
|
+
import { Result, errAsync, fromPromise, ok } from 'neverthrow';
|
|
2
3
|
import { AccountId } from 'polkadot-api';
|
|
3
4
|
import { defer, map, throwError } from 'rxjs';
|
|
4
5
|
import { toError } from '../helpers/utils.js';
|
|
5
6
|
import { zipWith } from '../helpers/zipWith.js';
|
|
6
|
-
|
|
7
|
+
import { IdentifierKey } from './identifierKey.js';
|
|
8
|
+
export function decodeRawIdentity(accountId, raw, textDecoder) {
|
|
7
9
|
if (!raw)
|
|
8
10
|
return null;
|
|
9
11
|
const credibility = raw.credibility.type === 'Lite'
|
|
@@ -11,13 +13,17 @@ function decodeRawIdentity(accountId, raw, textDecoder) {
|
|
|
11
13
|
: {
|
|
12
14
|
type: 'Person',
|
|
13
15
|
alias: raw.credibility.value.alias,
|
|
14
|
-
lastUpdate: raw.credibility.value.last_update
|
|
16
|
+
lastUpdate: raw.credibility.value.last_update?.toString() ?? null,
|
|
15
17
|
};
|
|
16
18
|
return {
|
|
17
19
|
accountId,
|
|
18
20
|
fullUsername: raw.full_username ? textDecoder.decode(raw.full_username) : null,
|
|
19
21
|
liteUsername: textDecoder.decode(raw.lite_username),
|
|
20
22
|
credibility,
|
|
23
|
+
// A keypair type we can't encrypt to is a normal condition, not a fault.
|
|
24
|
+
identifierKey: Result.fromThrowable(IdentifierKey.dec)(raw.identifier_key)
|
|
25
|
+
.map(({ value }) => toHex(value.key))
|
|
26
|
+
.unwrapOr(null),
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
29
|
export function createIdentityRpcAdapter(lazyClient) {
|
package/dist/identity/types.d.ts
CHANGED
|
@@ -5,13 +5,21 @@ export type Credibility = {
|
|
|
5
5
|
} | {
|
|
6
6
|
type: 'Person';
|
|
7
7
|
alias: `0x${string}`;
|
|
8
|
-
|
|
8
|
+
/** `null` when the chain record carries no readable timestamp. */
|
|
9
|
+
lastUpdate: string | null;
|
|
9
10
|
};
|
|
10
11
|
export type Identity = {
|
|
11
12
|
accountId: string;
|
|
12
13
|
fullUsername: string | null;
|
|
13
14
|
liteUsername: string;
|
|
14
15
|
credibility: Credibility;
|
|
16
|
+
/**
|
|
17
|
+
* The account's 32-byte X25519 chat encryption key, unwrapped from its RFC-0004
|
|
18
|
+
* container. `null` for a keypair type this SDK does not implement.
|
|
19
|
+
*
|
|
20
|
+
* Hex, not `Uint8Array`: `Identity` is JSON round-tripped through the storage cache.
|
|
21
|
+
*/
|
|
22
|
+
identifierKey: `0x${string}` | null;
|
|
15
23
|
};
|
|
16
24
|
export type IdentityAdapter = {
|
|
17
25
|
readIdentities(accounts: string[]): ResultAsync<Record<string, Identity | null>, Error>;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,9 +9,12 @@ export { AllowanceError } from './sso/allowance/index.js';
|
|
|
9
9
|
export type { AllowanceResourceKind } from './sso/allowance/index.js';
|
|
10
10
|
export type { UserSession } from './sso/sessionManager/userSession.js';
|
|
11
11
|
export type { StoredUserSession } from './sso/userSessionRepository.js';
|
|
12
|
-
export type { Identity } from './identity/types.js';
|
|
12
|
+
export type { Credibility, Identity, IdentityAdapter, IdentityRepository } from './identity/types.js';
|
|
13
|
+
export { createIdentityRepository } from './identity/impl.js';
|
|
14
|
+
export { createIdentityRpcAdapter } from './identity/rpcAdapter.js';
|
|
13
15
|
export type { SignRawLegacyRequest, SignRawLegacyResponse, SigningPayloadRequest, SigningPayloadResponse, SigningRawRequest, SigningRequest, } from './sso/sessionManager/scale/signing.js';
|
|
14
|
-
export type { RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, } from './sso/sessionManager/scale/ringVrf.js';
|
|
16
|
+
export type { RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, RingVrfSignRequest, RingVrfSignResponse, } from './sso/sessionManager/scale/ringVrf.js';
|
|
17
|
+
export type { ListRingVrfKeysRequest, ListRingVrfKeysResponse, RegisterRingVrfKeyRequest, RegisterRingVrfKeyResponse, } from './sso/sessionManager/scale/ringVrfKeys.js';
|
|
15
18
|
export type { SignVrfErr, SignVrfRequest, SignVrfResponse } from './sso/sessionManager/scale/signVrf.js';
|
|
16
19
|
export type { CreateTransactionLegacyRequest, CreateTransactionRequest, CreateTransactionResponse, } from './sso/sessionManager/scale/createTransaction.js';
|
|
17
20
|
export type { ProductSubtreeRequest, ProductSubtreeResponse } from './sso/sessionManager/scale/productSubtree.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { SS_PASEO_STABLE_STAGE_ENDPOINTS, SS_PREVIEW_STAGE_ENDPOINTS, SS_STABLE_STAGE_ENDPOINTS } from './constants.js';
|
|
2
2
|
export { createPappAdapter } from './papp.js';
|
|
3
3
|
export { AllowanceError } from './sso/allowance/index.js';
|
|
4
|
+
export { createIdentityRepository } from './identity/impl.js';
|
|
5
|
+
export { createIdentityRpcAdapter } from './identity/rpcAdapter.js';
|