@novasamatech/host-papp 0.9.0 → 0.9.2

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.
@@ -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,15 +215,15 @@ 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
221
  ## Ring VRF 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
+ A `UserSession` can ask the paired device for a privacy-preserving contextual alias, or a ring VRF proof, for a
224
+ product-scoped `context` and a `ring` location. The device selects the member key for the ring; `callingProductId` names
225
+ the product the host is acting for. Both take the same `(context, ring)` so the alias in the proof matches
226
+ `getRingVrfAlias`.
234
227
 
235
228
  ```ts
236
229
  // [productId, suffix]. The suffix is the wire `Index(u32) | Raw([u8; 32])` selector
@@ -255,11 +248,9 @@ proof.match(
255
248
 
256
249
  ## Product subtree public keys
257
250
 
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.
251
+ Product accounts live at `//product//{productId}/{index}` (RFC 0022). The product junction is **hard**, so the user's
252
+ root public key alone no longer determines product account public keys the host asks the paired device for the
253
+ product-subtree public key once, then soft-derives account public keys locally from it.
263
254
 
264
255
  ```ts
265
256
  const subtreeKey = await currentSession.getProductSubtree('product.dot');
@@ -270,15 +261,14 @@ subtreeKey.match(
270
261
  );
271
262
  ```
272
263
 
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.
264
+ The request is consent-free — the response carries no secret material. Only `AutoSigning` does: its payload is now the
265
+ product-subtree secret key alone (`productRootPrivateKey`, 64-byte expanded sr25519 secret), which exposes exactly that
266
+ product's subtree. The former `productDerivationSecret` is gone.
277
267
 
278
268
  ## Identity lookups
279
269
 
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).
270
+ `papp.identity` resolves on-chain identity data (lite / full username, credibility, chat encryption key) for arbitrary
271
+ account ids. Pass an `0x`-prefixed account id (32-byte hex).
282
272
 
283
273
  ```ts
284
274
  const lookup = async (accountId: string) => {
@@ -296,34 +286,51 @@ const lookup = async (accountId: string) => {
296
286
  await papp.identity.getIdentities([accountIdA, accountIdB]);
297
287
  ```
298
288
 
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:
289
+ A paired `UserSession` also exposes `getIdentity()` as a shortcut that looks up the identity of its own user identity
290
+ account — no account id to pass:
301
291
 
302
292
  ```ts
303
293
  const identity = await session.getIdentity(); // Result<Identity | null, Error>
304
294
  ```
305
295
 
296
+ `Identity` is:
297
+
298
+ ```ts
299
+ type Identity = {
300
+ accountId: string;
301
+ fullUsername: string | null;
302
+ liteUsername: string;
303
+ credibility: { type: 'Lite' } | { type: 'Person'; alias: `0x${string}`; lastUpdate: string | null };
304
+ /** 32-byte X25519 chat encryption key as hex, unwrapped from the 65-byte
305
+ * RFC-0004 container; null for a keypair type this SDK does not implement. */
306
+ identifierKey: `0x${string}` | null;
307
+ };
308
+ ```
309
+
310
+ `lastUpdate` and `identifierKey` are both nullable — the chain record can carry neither a readable timestamp nor a key
311
+ this SDK can use, and neither case is an error.
312
+
313
+ This package owns the `Resources.Consumers` reader for the monorepo. `host-chat` reads through it rather than querying
314
+ the chain itself; `createIdentityRpcAdapter` is exported for that, alongside the `Identity` and `Credibility` types.
315
+
306
316
  ## V2 SSO handshake
307
317
 
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.
318
+ V2 is a redesign of the SSO pairing flow that supports the same user identity across multiple devices. The host
319
+ generates a stable device keypair locally, emits a `VersionedHandshakeProposal::V2` via QR/deeplink, and an authorising
320
+ peer (e.g. the user's existing Polkadot App) responds over the Statement Store with the user identity keys signed to
321
+ authorise this device. Subsequent devices belonging to the same user reuse the same identity, so contacts, chats, and
322
+ roster events are shared between them.
314
323
 
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.
324
+ V2 is **not interoperable with V1**: a V1-only peer can't decode a V2 proposal QR and vice versa. Hosts that want to
325
+ support both should branch on which protocol the peer advertises.
317
326
 
318
327
  ### The flow
319
328
 
320
329
  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`.
330
+ 2. The authorising device scans it and posts its response to the Statement Store: first a `Pending` acknowledgement,
331
+ then either `Success` — carrying the user's identity keys, signed to authorise this device — or `Failed`.
324
332
  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.
333
+ `Submitted → Pending → Success | Failed` state machine. On `Success` it persists the user identity.
327
334
 
328
335
  ### Building and rendering the QR
329
336
 
@@ -333,7 +340,7 @@ import { buildPairingDeeplink } from '@novasamatech/host-papp';
333
340
  const deeplink = buildPairingDeeplink(
334
341
  {
335
342
  statementAccountPublicKey: device.statementAccountPublicKey, // sr25519 device pubkey, 32 bytes
336
- encryptionPublicKey: device.encryptionPublicKey, // P-256 device pubkey, 65 bytes uncompressed
343
+ encryptionPublicKey: device.encryptionPublicKey, // P-256 device pubkey, 65 bytes uncompressed
337
344
  },
338
345
  {
339
346
  hostName: 'My Host App',
@@ -396,9 +403,9 @@ pairing.abort();
396
403
 
397
404
  ### Surviving reloads / proper logout
398
405
 
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:
406
+ The chain holds the most recent statement on the pairing topic indefinitely, so on cold start the service will see the
407
+ previous Success and replay it. To distinguish a stale replay from a fresh re-pair, callers can pass byte-level dedupe
408
+ state:
402
409
 
403
410
  ```ts
404
411
  const pairing = startPairingV2({
@@ -410,14 +417,13 @@ const pairing = startPairingV2({
410
417
  });
411
418
  ```
412
419
 
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.
420
+ The service skips any incoming statement whose bytes match `initialProcessedDataHex`. PApp re-encrypts every Success
421
+ with a fresh ephemeral key + AES-GCM nonce, so a genuine re-pair always produces different bytes and passes the dedupe.
416
422
 
417
423
  ## Reading allowances
418
424
 
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:
425
+ Each `UserSession` can read its own persisted allowance slot-account key for a given product and resource. The session
426
+ id is implicit — you only pass the product and resource:
421
427
 
422
428
  ```ts
423
429
  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,7 +9,9 @@ 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
16
  export type { RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, } from './sso/sessionManager/scale/ringVrf.js';
15
17
  export type { SignVrfErr, SignVrfRequest, SignVrfResponse } from './sso/sessionManager/scale/signVrf.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';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/host-papp",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.9.2",
5
5
  "description": "Polkadot app integration",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -11,6 +11,10 @@
11
11
  "keywords": [
12
12
  "polkadot"
13
13
  ],
14
+ "scripts": {
15
+ "papi:fix": "node ../../scripts/fix-papi-descriptors.mjs .papi/descriptors/dist",
16
+ "papi:update": "papi update && npm run papi:fix"
17
+ },
14
18
  "main": "dist/index.js",
15
19
  "exports": {
16
20
  "./package.json": "./package.json",
@@ -34,10 +38,10 @@
34
38
  "@noble/ciphers": "2.2.0",
35
39
  "@noble/curves": "2.2.0",
36
40
  "@noble/hashes": "2.2.0",
37
- "@novasamatech/host-api": "0.9.0",
38
- "@novasamatech/scale": "0.9.0",
39
- "@novasamatech/statement-store": "0.9.0",
40
- "@novasamatech/storage-adapter": "0.9.0",
41
+ "@novasamatech/host-api": "0.9.2",
42
+ "@novasamatech/scale": "0.9.2",
43
+ "@novasamatech/statement-store": "0.9.2",
44
+ "@novasamatech/storage-adapter": "0.9.2",
41
45
  "@polkadot-api/utils": "^0.4.0",
42
46
  "@polkadot-labs/hdkd-helpers": "^0.0.31",
43
47
  "nanoevents": "10.0.0",