@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.
Files changed (37) hide show
  1. package/.papi/descriptors/dist/common-types.d.ts +1483 -1359
  2. package/.papi/descriptors/dist/descriptors-BLqmZ_vS.js +25 -0
  3. package/.papi/descriptors/dist/index.d.ts +1 -1
  4. package/.papi/descriptors/dist/index.js +8 -6
  5. package/.papi/descriptors/dist/metadataTypes-wXgvb64i.js +3 -0
  6. package/.papi/descriptors/dist/metadataTypes.d.ts +1 -1
  7. package/.papi/descriptors/dist/people_lite.d.ts +1175 -1001
  8. package/.papi/descriptors/dist/people_lite_metadata-CwU9OBGy.js +3 -0
  9. package/.papi/descriptors/generated.json +1 -1
  10. package/.papi/descriptors/package.json +1 -1
  11. package/.papi/metadata/people_lite.scale +0 -0
  12. package/.papi/polkadot-api.json +3 -3
  13. package/README.md +140 -87
  14. package/dist/identity/identifierKey.d.ts +19 -0
  15. package/dist/identity/identifierKey.js +17 -0
  16. package/dist/identity/impl.js +12 -17
  17. package/dist/identity/index.d.ts +2 -0
  18. package/dist/identity/index.js +1 -0
  19. package/dist/identity/rpcAdapter.d.ts +5 -1
  20. package/dist/identity/rpcAdapter.js +9 -3
  21. package/dist/identity/types.d.ts +9 -1
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.js +2 -0
  24. package/dist/sso/sessionManager/scale/remoteMessage.d.ts +102 -2
  25. package/dist/sso/sessionManager/scale/remoteMessage.js +11 -1
  26. package/dist/sso/sessionManager/scale/resourceAllocation.d.ts +3 -0
  27. package/dist/sso/sessionManager/scale/resourceAllocation.js +11 -0
  28. package/dist/sso/sessionManager/scale/ringVrf.d.ts +43 -2
  29. package/dist/sso/sessionManager/scale/ringVrf.js +12 -1
  30. package/dist/sso/sessionManager/scale/ringVrfKeys.d.ts +85 -0
  31. package/dist/sso/sessionManager/scale/ringVrfKeys.js +22 -0
  32. package/dist/sso/sessionManager/userSession.d.ts +22 -3
  33. package/dist/sso/sessionManager/userSession.js +59 -3
  34. package/package.json +9 -5
  35. package/.papi/descriptors/dist/descriptors-D5oN4i7I.js +0 -25
  36. package/.papi/descriptors/dist/metadataTypes-BaUy_4gO.js +0 -3
  37. package/.papi/descriptors/dist/people_lite_metadata-Cj1JLFI2.js +0 -3
@@ -1 +1 @@
1
- {"cliVersion":"0.21.1","whitelist":null,"chains":{"people_lite":"0x5856bcc2600f139c76f7af463e725b08"},"contracts":{"ink":{},"sol":{}}}
1
+ {"cliVersion":"0.21.9","whitelist":null,"chains":{"people_lite":"0xcca79c4482b2233e7cd4d24a23988e77"},"contracts":{"ink":{},"sol":{}}}
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-autogenerated.11805297896621906130",
2
+ "version": "0.1.0-autogenerated.15132890757373389519",
3
3
  "name": "@polkadot-api/descriptors",
4
4
  "files": [
5
5
  "dist"
Binary file
@@ -6,10 +6,10 @@
6
6
  },
7
7
  "entries": {
8
8
  "people_lite": {
9
- "wsUrl": "wss://paseo-people-next-rpc.polkadot.io",
9
+ "wsUrl": "wss://people-paseo.gatotech.network",
10
10
  "metadata": ".papi/metadata/people_lite.scale",
11
- "genesis": "0xa22a2424d2cbf561eaecf7da8b1b548fa9d1939f60265e942b1049616a012f71",
12
- "codeHash": "0x0e48da626edc33a96d18d81fdc5eebe5d6a9b624af4ca4b9dbcc1345a3b03bb6"
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
- Mobile capabilities. It encapsulates everything needed to:
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
- (`subscribe` / `read`), so it can be wired into React, Vue, Svelte, vanilla DOM, or a
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
- via the `adapters` option for testing or non-browser environments.
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
- `papp.sso.authenticate()`. For the multi-device V2 protocol see
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
- stored user session, or `null` if the flow was aborted. The flow is idempotent — calling it
68
- again while a previous run is in flight returns the same in-progress promise.
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
- status is `pairing`, then transitions to a "signing in" screen during attestation.
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
- attestation progress (`attestation` with a claimed `username`, `attestationError`, or
126
- `finished`). For convenience, treat the two streams as a single derived UI state — pairing
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
- work with the first one (single-user model), but the SDK does not enforce that.
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
- (RFC-0023). The transcript travels as a recipe — a root domain-separation label plus an
202
- ordered list of `(label, value)` items — which the wallet replays verbatim into a Merlin
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
- signs locally and never round-trips to the wallet.
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
- A `UserSession` can ask the paired device for a privacy-preserving contextual alias, or a
231
- ring VRF proof, for a product-scoped `context` and a `ring` location. The device
232
- selects the member key for the ring; `callingProductId` names the product the host is acting
233
- for. Both take the same `(context, ring)` so the alias in the proof matches `getRingVrfAlias`.
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
- const alias = await currentSession.getRingVrfAlias('caller.dot', context, ring);
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('caller.dot', context, ring, new Uint8Array([0x48, 0x69]));
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 `RingVrfError` (RingNotFound / NotMember / Rejected / Unknown)
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 junction is **hard**, so the user's root public key alone no longer
260
- determines product account public keys — the host asks the paired device for the
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
- `AutoSigning` does: its payload is now the product-subtree secret key alone
275
- (`productRootPrivateKey`, 64-byte expanded sr25519 secret), which exposes exactly
276
- that product's subtree. The former `productDerivationSecret` is gone.
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, slots)
281
- for arbitrary account ids. Pass an `0x`-prefixed account id (32-byte hex).
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
- of its own user identity account — no account id to pass:
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
- multiple devices. The host generates a stable device keypair locally, emits a
310
- `VersionedHandshakeProposal::V2` via QR/deeplink, and an authorising peer (e.g. the user's
311
- existing Polkadot App) responds over the Statement Store with the user identity keys signed
312
- to authorise this device. Subsequent devices belonging to the same user reuse the same
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
- versa. Hosts that want to support both should branch on which protocol the peer advertises.
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
- `Pending` acknowledgement, then either `Success` — carrying the user's identity keys,
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, // P-256 device pubkey, 65 bytes uncompressed
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
- start the service will see the previous Success and replay it. To distinguish a stale
401
- replay from a fresh re-pair, callers can pass byte-level dedupe state:
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
- re-encrypts every Success with a fresh ephemeral key + AES-GCM nonce, so a genuine re-pair
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
- product and resource. The session id is implicit — you only pass the product and resource:
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
+ });
@@ -1,15 +1,20 @@
1
- import { Result, ResultAsync, err, ok, okAsync } from 'neverthrow';
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
- return JSON.parse(raw);
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, getCacheKey);
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, getKey) {
89
+ function createCachedIdentityRequester(storage) {
85
90
  function readSingleCacheRecord(accountId) {
86
- return storage.read(getKey(accountId)).andThen(raw => {
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(getKey(accountId), JSON.stringify(identity));
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,2 @@
1
+ export type { Credibility, Identity } from './types.js';
2
+ export { createIdentityRpcAdapter } from './rpcAdapter.js';
@@ -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 { IdentityAdapter } from './types.js';
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 { errAsync, fromPromise, ok } from 'neverthrow';
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
- function decodeRawIdentity(accountId, raw, textDecoder) {
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.toString(),
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) {
@@ -5,13 +5,21 @@ export type Credibility = {
5
5
  } | {
6
6
  type: 'Person';
7
7
  alias: `0x${string}`;
8
- lastUpdate: string;
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';