@novasamatech/host-papp 0.8.11 → 0.9.0

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 (38) hide show
  1. package/README.md +84 -69
  2. package/dist/crypto.js +8 -11
  3. package/dist/crypto.spec.d.ts +1 -0
  4. package/dist/crypto.spec.js +31 -0
  5. package/dist/helpers/createAsyncTaskPool.spec.js +11 -11
  6. package/dist/index.d.ts +3 -0
  7. package/dist/papp.js +3 -1
  8. package/dist/sso/allowance/repository.js +2 -1
  9. package/dist/sso/auth/scale/handshakeV2.d.ts +5 -5
  10. package/dist/sso/auth/scale/handshakeV2.js +11 -10
  11. package/dist/sso/auth/v2/envelope.d.ts +6 -6
  12. package/dist/sso/auth/v2/envelope.js +9 -9
  13. package/dist/sso/auth/v2/state.d.ts +2 -2
  14. package/dist/sso/auth/v2/topic.d.ts +1 -1
  15. package/dist/sso/auth/v2/topic.js +1 -1
  16. package/dist/sso/deviceIdentityStore.js +2 -1
  17. package/dist/sso/sessionManager/impl.d.ts +3 -1
  18. package/dist/sso/sessionManager/impl.js +6 -4
  19. package/dist/sso/sessionManager/scale/createTransaction.d.ts +9 -3
  20. package/dist/sso/sessionManager/scale/createTransaction.js +2 -2
  21. package/dist/sso/sessionManager/scale/productSubtree.d.ts +12 -0
  22. package/dist/sso/sessionManager/scale/productSubtree.js +22 -0
  23. package/dist/sso/sessionManager/scale/remoteMessage.d.ts +100 -24
  24. package/dist/sso/sessionManager/scale/remoteMessage.js +11 -1
  25. package/dist/sso/sessionManager/scale/resourceAllocation.d.ts +14 -5
  26. package/dist/sso/sessionManager/scale/resourceAllocation.js +8 -3
  27. package/dist/sso/sessionManager/scale/ringVrf.d.ts +18 -29
  28. package/dist/sso/sessionManager/scale/ringVrf.js +5 -13
  29. package/dist/sso/sessionManager/scale/signVrf.d.ts +62 -0
  30. package/dist/sso/sessionManager/scale/signVrf.js +18 -0
  31. package/dist/sso/sessionManager/scale/signing.d.ts +39 -15
  32. package/dist/sso/sessionManager/scale/signing.js +2 -2
  33. package/dist/sso/sessionManager/userSession.d.ts +19 -2
  34. package/dist/sso/sessionManager/userSession.js +45 -2
  35. package/dist/sso/userSecretRepository.js +2 -1
  36. package/dist/sso/userSessionRepository.js +13 -8
  37. package/dist/sso/userSessionRepository.spec.js +4 -4
  38. package/package.json +8 -8
package/README.md CHANGED
@@ -35,10 +35,6 @@ const papp = createPappAdapter({
35
35
  // otherwise existing pairings will be lost.
36
36
  appId: 'my-host-app',
37
37
 
38
- // URL to a JSON document describing the host: { name: string, icon: string }.
39
- // The icon should be a rasterized image at least 256x256 px.
40
- metadata: 'https://my-host-app.example/papp-metadata.json',
41
-
42
38
  // Optional environment metadata shown on the wallet's confirmation screen.
43
39
  hostMetadata: {
44
40
  hostVersion: '1.4.0',
@@ -48,14 +44,15 @@ const papp = createPappAdapter({
48
44
  });
49
45
  ```
50
46
 
51
- `createPappAdapter` returns four sub-modules:
47
+ `createPappAdapter` returns five sub-modules:
52
48
 
53
- | Module | Purpose |
54
- | --------------- | ---------------------------------------------------------------------- |
55
- | `papp.sso` | Authentication / pairing flow with a remote wallet. |
56
- | `papp.sessions` | List of paired user sessions and per-session messaging (sign, etc.). |
57
- | `papp.secrets` | Local secret storage for the derived guest accounts. |
58
- | `papp.identity` | On-chain identity lookups for arbitrary account ids. |
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.|
59
56
 
60
57
  Custom adapters (statement store, identity RPC, storage, lazy chain client) can be supplied
61
58
  via the `adapters` option for testing or non-browser environments.
@@ -200,6 +197,34 @@ await currentSession.signRaw({
200
197
  });
201
198
  ```
202
199
 
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;
204
+ the host never injects it.
205
+
206
+ ```ts
207
+ const encoder = new TextEncoder();
208
+
209
+ const vrf = await currentSession.signVrf({
210
+ productAccountId: ['product.dot', 0],
211
+ productId: 'product.dot',
212
+ transcriptLabel: encoder.encode('pop:airdrop'),
213
+ items: [
214
+ { label: encoder.encode('domain'), value: domainBytes },
215
+ { label: encoder.encode('signer'), value: accountPublicKey },
216
+ ],
217
+ });
218
+
219
+ vrf.match(
220
+ ({ preOutput, proof }) => submitLotteryTicket(preOutput, proof),
221
+ error => console.error('VRF signing failed:', error),
222
+ );
223
+ ```
224
+
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.
227
+
203
228
  ## Ring VRF proofs and aliases
204
229
 
205
230
  A `UserSession` can ask the paired device for a privacy-preserving contextual alias, or a
@@ -208,7 +233,10 @@ selects the member key for the ring; `callingProductId` names the product the ho
208
233
  for. Both take the same `(context, ring)` so the alias in the proof matches `getRingVrfAlias`.
209
234
 
210
235
  ```ts
211
- const context = ['product.dot', '0x00']; // [productId, suffix] suffix is a 0x-hex string
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 }];
212
240
  const ring = {
213
241
  chainId: '0x…', // 32-byte chain genesis hash
214
242
  junctions: [{ tag: 'PalletInstance', value: 42 }],
@@ -225,6 +253,28 @@ proof.match(
225
253
  );
226
254
  ```
227
255
 
256
+ ## Product subtree public keys
257
+
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.
263
+
264
+ ```ts
265
+ const subtreeKey = await currentSession.getProductSubtree('product.dot');
266
+
267
+ subtreeKey.match(
268
+ publicKey => cacheProductSubtree('product.dot', publicKey), // one round trip per product, ever
269
+ error => console.error('subtree lookup failed:', error),
270
+ );
271
+ ```
272
+
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.
277
+
228
278
  ## Identity lookups
229
279
 
230
280
  `papp.identity` resolves on-chain identity data (lite / full username, credibility, slots)
@@ -246,6 +296,13 @@ const lookup = async (accountId: string) => {
246
296
  await papp.identity.getIdentities([accountIdA, accountIdB]);
247
297
  ```
248
298
 
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:
301
+
302
+ ```ts
303
+ const identity = await session.getIdentity(); // Result<Identity | null, Error>
304
+ ```
305
+
249
306
  ## V2 SSO handshake
250
307
 
251
308
  V2 is a redesign of the SSO pairing flow that supports the same user identity across
@@ -258,38 +315,15 @@ identity, so contacts, chats, and roster events are shared between them.
258
315
  V2 is **not interoperable with V1**: a V1-only peer can't decode a V2 proposal QR and vice
259
316
  versa. Hosts that want to support both should branch on which protocol the peer advertises.
260
317
 
261
- ### Shape of the flow
262
-
263
- ```
264
- host peer (authorising device)
265
- ────────────────────────────────────────────────────────────────────────
266
- buildPairingDeeplink(device, metadata)
267
- → polkadotapp://pair?handshake=<hex>
268
- scan QR, decode proposal
269
- compute pairing topic from
270
- the host's pubkeys
271
- ECDH-encrypt + post:
272
- Pending(AllowanceAllocation)
273
- Success { encryptionKey,
274
- accountId,
275
- identitySignature }
276
- Failed(reason)
277
- service.subscribeStatements(topic) +
278
- poll the topic every 2s
279
-
280
- decode VersionedHandshakeResponse::V2
281
- → ECDH-decrypt envelope with the
282
- device encryption private key
283
- → SCALE-decode inner payload
284
- → state machine: Submitted → Pending →
285
- Success | Failed
286
- → on Success persist user identity
287
- ```
318
+ ### The flow
288
319
 
289
- The user identity carried in `Success` is the chat encryption pubkey + the user's identity
290
- sr25519 accountId. The host verifies the 64-byte sr25519 `identitySignature` against the
291
- canonical 97 bytes `statementAccountId || encryptionPublicKey`
292
- (see `IDENTITY_SIGNATURE_PAYLOAD_BYTES`).
320
+ 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`.
324
+ 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.
293
327
 
294
328
  ### Building and rendering the QR
295
329
 
@@ -318,7 +352,7 @@ renderQrCode(deeplink); // 'polkadotapp://pair?handshake=<hex>'
318
352
  import { startPairingV2 } from '@novasamatech/host-papp';
319
353
 
320
354
  const pairing = startPairingV2({
321
- statementStore: papp.adapters.statementStore, // any StatementStoreAdapter
355
+ statementStore, // any StatementStoreAdapter
322
356
  deviceIdentity: {
323
357
  statementAccountPublicKey: device.statementAccountPublicKey,
324
358
  encryptionPublicKey: device.encryptionPublicKey,
@@ -380,33 +414,14 @@ The service skips any incoming statement whose bytes match `initialProcessedData
380
414
  re-encrypts every Success with a fresh ephemeral key + AES-GCM nonce, so a genuine re-pair
381
415
  always produces different bytes and passes the dedupe.
382
416
 
383
- ### Pairing topic / channel
417
+ ## Reading allowances
384
418
 
385
- If the host needs to derive the pairing topic or channel itself (for example to subscribe
386
- in-line, or to verify a statement source):
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:
387
421
 
388
422
  ```ts
389
- import { computePairingTopic, computePairingChannel } from '@novasamatech/host-papp';
423
+ const session = papp.sessions.sessions.read().at(0);
390
424
 
391
- const topic = computePairingTopic(statementAccountId, encryptionPublicKey);
392
- const channel = computePairingChannel(statementAccountId, encryptionPublicKey);
393
- // topic = blake2b256_keyed(encryptionPublicKey || "topic", key=statementAccountId)
394
- // channel = blake2b256_keyed(encryptionPublicKey || "channel", key=statementAccountId)
425
+ // resource: 'bulletin' | 'statementStore'
426
+ const key = await session.readAllowance(productId, 'statementStore'); // Result<Uint8Array | null, Error>
395
427
  ```
396
-
397
- ### Codec exports
398
-
399
- The SCALE codecs are exported as plain `Codec<T>` values for callers that need to
400
- encode/decode statements outside the orchestrator:
401
-
402
- | Export | Description |
403
- | --------------------------------- | -------------------------------------------------------------------------------------------- |
404
- | `VersionedHandshakeProposal` | Outer enum; V2 at SCALE discriminant 1, with `_v1Reserved` at 0. |
405
- | `HandshakeProposalV2` | `{ device, metadata }` — what the QR encodes. |
406
- | `Device` | `{ statementAccountId(32), encryptionPublicKey(65) }`. |
407
- | `MetadataKey`, `MetadataEntry` | Metadata enum + `(MetadataKey, str)` tuple. |
408
- | `VersionedHandshakeResponse` | Outer enum for the answer; `V1` legacy + `V2`. |
409
- | `HandshakeResponseV2` | `{ encrypted, tmpKey(65) }` — the ECDH-wrapped envelope. |
410
- | `EncryptedHandshakeResponseV2` | Inner payload after envelope decrypt: `Pending` (1 byte), `Success` (161 bytes), `Failed`. |
411
- | `HandshakeSuccessV2` | `{ encryptionKey(65), accountId(32), identitySignature(64) }`. |
412
- | `IDENTITY_SIGNATURE_PAYLOAD_BYTES`| `97` — the bytes the user identity sr25519 signs over. |
package/dist/crypto.js CHANGED
@@ -1,13 +1,13 @@
1
- import { p256 } from '@noble/curves/nist.js';
1
+ import { x25519 } from '@noble/curves/ed25519.js';
2
+ import { Bytes } from '@novasamatech/scale';
2
3
  import { createSr25519Secret, deriveSr25519PublicKey, signWithSr25519Secret, verifySr25519Signature, } from '@novasamatech/statement-store';
3
4
  import { entropyToMiniSecret, mnemonicToEntropy } from '@polkadot-labs/hdkd-helpers';
4
- import { Bytes } from 'scale-ts';
5
5
  // schemas
6
6
  export function BrandedBytesCodec(length) {
7
7
  return Bytes(length);
8
8
  }
9
9
  export const SsPubKey = BrandedBytesCodec(32);
10
- export const EncrPubKey = BrandedBytesCodec(65);
10
+ export const EncrPubKey = BrandedBytesCodec(32);
11
11
  // helpers
12
12
  const textEncoder = new TextEncoder();
13
13
  export function stringToBytes(str) {
@@ -27,16 +27,13 @@ export function deriveSr25519Account(mnemonic, derivation) {
27
27
  }
28
28
  // encryption key pair
29
29
  export function createEncrSecret(entropy) {
30
- const miniSecret = entropyToMiniSecret(entropy);
31
- const seed = new Uint8Array(48);
32
- seed.set(miniSecret);
33
- const { secretKey } = p256.keygen(seed);
34
- return secretKey;
30
+ // The 32-byte mini-secret is the X25519 private scalar (clamped internally by @noble on use).
31
+ return entropyToMiniSecret(entropy);
35
32
  }
36
33
  export function getEncrPub(secret) {
37
- return p256.getPublicKey(secret, false);
34
+ return x25519.getPublicKey(secret);
38
35
  }
39
36
  export function createSharedSecret(secret, publicKey) {
40
- // slicing first byte: @noble/curves adds y offset at the start
41
- return p256.getSharedSecret(secret, publicKey).slice(1, 33);
37
+ // The X25519 output is used whole. @noble aborts on an all-zero (small-order) result per RFC 7748.
38
+ return x25519.getSharedSecret(secret, publicKey);
42
39
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createSharedSecret, getEncrPub } from './crypto.js';
3
+ // RFC 7748 §5.2 X25519 test vector.
4
+ const fromHex = (hex) => Uint8Array.from(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
5
+ const ALICE_PRIVATE = '77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a';
6
+ const ALICE_PUBLIC = '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a';
7
+ const BOB_PRIVATE = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
8
+ const BOB_PUBLIC = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
9
+ const SHARED_SECRET = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
10
+ const asSecret = (hex) => fromHex(hex);
11
+ describe('X25519 encryption keys', () => {
12
+ it('derives a 32-byte X25519 public key from a private scalar (RFC 7748 vector)', () => {
13
+ const publicKey = getEncrPub(asSecret(ALICE_PRIVATE));
14
+ expect(publicKey.length).toBe(32);
15
+ expect(publicKey).toEqual(fromHex(ALICE_PUBLIC));
16
+ });
17
+ it('computes the RFC 7748 shared secret whole, without slicing', () => {
18
+ const shared = createSharedSecret(asSecret(ALICE_PRIVATE), fromHex(BOB_PUBLIC));
19
+ expect(shared.length).toBe(32);
20
+ expect(shared).toEqual(fromHex(SHARED_SECRET));
21
+ });
22
+ it('agrees on the same shared secret from either side', () => {
23
+ const fromAlice = createSharedSecret(asSecret(ALICE_PRIVATE), fromHex(BOB_PUBLIC));
24
+ const fromBob = createSharedSecret(asSecret(BOB_PRIVATE), fromHex(ALICE_PUBLIC));
25
+ expect(fromAlice).toEqual(fromBob);
26
+ });
27
+ it('rejects a small-order public key (all-zero shared secret)', () => {
28
+ const smallOrderPoint = new Uint8Array(32);
29
+ expect(() => createSharedSecret(asSecret(ALICE_PRIVATE), smallOrderPoint)).toThrow();
30
+ });
31
+ });
@@ -1,5 +1,5 @@
1
1
  import { setTimeout } from 'node:timers/promises';
2
- import { err, fromPromise, ok, okAsync } from 'neverthrow';
2
+ import { fromPromise, okAsync } from 'neverthrow';
3
3
  import { describe, expect, it, vi } from 'vitest';
4
4
  import { createAsyncTaskPool } from './createAsyncTaskPool.js';
5
5
  import { toError } from './utils.js';
@@ -8,7 +8,7 @@ describe('asyncTaskPool', () => {
8
8
  it('should exec async task', async () => {
9
9
  const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: () => 0 });
10
10
  const result = await pool.call(() => fromPromise(delay().then(() => 'test'), toError));
11
- expect(result).toEqual(ok('test'));
11
+ await expect(result).toBeOkWith('test');
12
12
  });
13
13
  it('should handle sync errors', async () => {
14
14
  const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: () => 0 });
@@ -16,13 +16,13 @@ describe('asyncTaskPool', () => {
16
16
  const result = await pool.call(() => {
17
17
  throw error;
18
18
  });
19
- expect(result).toEqual(err(error));
19
+ await expect(result).toBeErrWith(error);
20
20
  });
21
21
  it('should handle async errors', async () => {
22
22
  const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: () => 0 });
23
23
  const error = new Error('test');
24
24
  const result = await pool.call(() => fromPromise(Promise.reject(error), toError));
25
- return expect(result).toEqual(err(error));
25
+ await expect(result).toBeErrWith(error);
26
26
  });
27
27
  it('should handle queue', async () => {
28
28
  const pool = createAsyncTaskPool({ poolSize: 2, retryCount: 0, retryDelay: () => 0 });
@@ -52,7 +52,7 @@ describe('asyncTaskPool', () => {
52
52
  tries++;
53
53
  throw new Error();
54
54
  });
55
- expect(result).toEqual(ok('test'));
55
+ await expect(result).toBeOkWith('test');
56
56
  });
57
57
  it('should throw on retry limit exceeding', async () => {
58
58
  const spy = vi.fn(() => 0);
@@ -66,7 +66,7 @@ describe('asyncTaskPool', () => {
66
66
  throw new Error();
67
67
  });
68
68
  expect(spy).toBeCalledTimes(1);
69
- expect(result).toEqual(err(new Error()));
69
+ await expect(result).toBeErrWith(new Error());
70
70
  });
71
71
  it('should correctly calculate retry delay', async () => {
72
72
  const spy = vi.fn((retry) => retry * 10);
@@ -125,9 +125,9 @@ describe('asyncTaskPool', () => {
125
125
  const queued = pool.call(queuedSpy, { signal: controller.signal });
126
126
  controller.abort();
127
127
  const queuedResult = await queued;
128
- expect(queuedResult.isErr()).toBe(true);
128
+ await expect(queuedResult).toBeErr();
129
129
  expect(queuedSpy).not.toHaveBeenCalled();
130
- expect((await active).isOk()).toBe(true);
130
+ await expect(await active).toBeOk();
131
131
  });
132
132
  it('rejects the in-flight active task when the signal aborts', async () => {
133
133
  const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
@@ -136,7 +136,7 @@ describe('asyncTaskPool', () => {
136
136
  signal: controller.signal,
137
137
  });
138
138
  controller.abort();
139
- expect((await active).isErr()).toBe(true);
139
+ await expect(await active).toBeErr();
140
140
  });
141
141
  it('frees the slot for later tasks after an abort', async () => {
142
142
  const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
@@ -147,7 +147,7 @@ describe('asyncTaskPool', () => {
147
147
  controller.abort();
148
148
  await aborted;
149
149
  const next = await pool.call(() => okAsync('next'));
150
- expect(next).toEqual(ok('next'));
150
+ await expect(next).toBeOkWith('next');
151
151
  });
152
152
  it('rejects immediately when called with an already-aborted signal', async () => {
153
153
  const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
@@ -155,7 +155,7 @@ describe('asyncTaskPool', () => {
155
155
  controller.abort();
156
156
  const spy = vi.fn(() => okAsync('x'));
157
157
  const result = await pool.call(spy, { signal: controller.signal });
158
- expect(result.isErr()).toBe(true);
158
+ await expect(result).toBeErr();
159
159
  expect(spy).not.toHaveBeenCalled();
160
160
  });
161
161
  });
package/dist/index.d.ts CHANGED
@@ -6,9 +6,12 @@ export type { PairingStatus } from './sso/auth/types.js';
6
6
  export type { DeviceIdentityForPairing } from './sso/auth/v2/service.js';
7
7
  export type { AllowanceErrorReason, AllowanceService } from './sso/allowance/index.js';
8
8
  export { AllowanceError } from './sso/allowance/index.js';
9
+ export type { AllowanceResourceKind } from './sso/allowance/index.js';
9
10
  export type { UserSession } from './sso/sessionManager/userSession.js';
10
11
  export type { StoredUserSession } from './sso/userSessionRepository.js';
11
12
  export type { Identity } from './identity/types.js';
12
13
  export type { SignRawLegacyRequest, SignRawLegacyResponse, SigningPayloadRequest, SigningPayloadResponse, SigningRawRequest, SigningRequest, } from './sso/sessionManager/scale/signing.js';
13
14
  export type { RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, } from './sso/sessionManager/scale/ringVrf.js';
15
+ export type { SignVrfErr, SignVrfRequest, SignVrfResponse } from './sso/sessionManager/scale/signVrf.js';
14
16
  export type { CreateTransactionLegacyRequest, CreateTransactionRequest, CreateTransactionResponse, } from './sso/sessionManager/scale/createTransaction.js';
17
+ export type { ProductSubtreeRequest, ProductSubtreeResponse } from './sso/sessionManager/scale/productSubtree.js';
package/dist/papp.js CHANGED
@@ -19,6 +19,7 @@ export function createPappAdapter({ appId, hostMetadata, deviceIdentity, onAuthS
19
19
  const ssoSessionRepository = createUserSessionRepository(storage);
20
20
  const userSecretRepository = createUserSecretRepository(appId, storage);
21
21
  const allowanceRepository = createAllowanceRepository(appId, storage);
22
+ const identityRepository = createIdentityRepository({ adapter: identities, storage });
22
23
  const deviceIdentityStore = createDeviceIdentityStore(appId, storage);
23
24
  const sessions = createSsoSessionManager({
24
25
  storage,
@@ -26,6 +27,7 @@ export function createPappAdapter({ appId, hostMetadata, deviceIdentity, onAuthS
26
27
  ssoSessionRepository,
27
28
  userSecretRepository,
28
29
  allowanceRepository,
30
+ identityRepository,
29
31
  });
30
32
  return {
31
33
  sso: createAuth({
@@ -39,7 +41,7 @@ export function createPappAdapter({ appId, hostMetadata, deviceIdentity, onAuthS
39
41
  }),
40
42
  sessions,
41
43
  secrets: userSecretRepository,
42
- identity: createIdentityRepository({ adapter: identities, storage }),
44
+ identity: identityRepository,
43
45
  allowance: createAllowanceService({ sessions: sessions.sessions, repository: allowanceRepository }),
44
46
  };
45
47
  }
@@ -1,8 +1,9 @@
1
1
  import { gcm } from '@noble/ciphers/aes.js';
2
2
  import { blake2b } from '@noble/hashes/blake2.js';
3
+ import { Bytes } from '@novasamatech/scale';
3
4
  import { fromThrowable } from 'neverthrow';
4
5
  import { fromHex, toHex } from 'polkadot-api/utils';
5
- import { Bytes, Enum, Struct, Vector, _void, str } from 'scale-ts';
6
+ import { Enum, Struct, Vector, _void, str } from 'scale-ts';
6
7
  import { stringToBytes } from '../../crypto.js';
7
8
  import { toError } from '../../helpers/utils.js';
8
9
  const AllowanceResourceKindCodec = Enum({
@@ -16,14 +16,14 @@
16
16
  * for soft-derivation of product accounts; PApp
17
17
  * and host MUST derive identically so a dapp sees
18
18
  * the same address on every device.
19
- * - `identityChatPrivateKey`— user identity chat P-256 private scalar (32 bytes),
19
+ * - `identityChatPrivateKey`— user identity chat X25519 private scalar (32 bytes),
20
20
  * shared per the multi-device spec so this device
21
21
  * can decrypt traffic addressed to the user identity
22
- * - `deviceEncPubKey` — encryption public key of the PApp device (65 bytes,
23
- * P-256 uncompressed). Tells the host which key to
22
+ * - `deviceEncPubKey` — encryption public key of the PApp device (32 bytes,
23
+ * X25519). Tells the host which key to
24
24
  * use when addressing chat envelopes back to the
25
25
  * authorising PApp device.
26
- * - `ssoEncPubKey` — `papp_encr_pub` (65 bytes, P-256 uncompressed); see
26
+ * - `ssoEncPubKey` — `papp_encr_pub` (32 bytes, X25519); see
27
27
  * the `HandshakeSuccessV2` codec doc below.
28
28
  * - `rootEntropySource` — 32 bytes; `blake2b256_keyed(rootAccountSecret,
29
29
  * "product-entropy-derivation")` per RFC-0007 (layer 1).
@@ -137,7 +137,7 @@ export declare const HandshakeSuccessV2: import("scale-ts").Codec<{
137
137
  deviceEncPubKey: Uint8Array<ArrayBufferLike>;
138
138
  rootEntropySource: Uint8Array<ArrayBufferLike>;
139
139
  }>;
140
- /** Derive the identity chat P-256 public key (uncompressed) from its private scalar. */
140
+ /** Derive the identity chat X25519 public key (32 bytes) from its private scalar. */
141
141
  export declare const deriveIdentityChatPublicKey: (privateKey: Uint8Array) => Uint8Array;
142
142
  export declare const HandshakeStatusV2: import("scale-ts").Codec<{
143
143
  tag: "AllowanceAllocation";
@@ -16,14 +16,14 @@
16
16
  * for soft-derivation of product accounts; PApp
17
17
  * and host MUST derive identically so a dapp sees
18
18
  * the same address on every device.
19
- * - `identityChatPrivateKey`— user identity chat P-256 private scalar (32 bytes),
19
+ * - `identityChatPrivateKey`— user identity chat X25519 private scalar (32 bytes),
20
20
  * shared per the multi-device spec so this device
21
21
  * can decrypt traffic addressed to the user identity
22
- * - `deviceEncPubKey` — encryption public key of the PApp device (65 bytes,
23
- * P-256 uncompressed). Tells the host which key to
22
+ * - `deviceEncPubKey` — encryption public key of the PApp device (32 bytes,
23
+ * X25519). Tells the host which key to
24
24
  * use when addressing chat envelopes back to the
25
25
  * authorising PApp device.
26
- * - `ssoEncPubKey` — `papp_encr_pub` (65 bytes, P-256 uncompressed); see
26
+ * - `ssoEncPubKey` — `papp_encr_pub` (32 bytes, X25519); see
27
27
  * the `HandshakeSuccessV2` codec doc below.
28
28
  * - `rootEntropySource` — 32 bytes; `blake2b256_keyed(rootAccountSecret,
29
29
  * "product-entropy-derivation")` per RFC-0007 (layer 1).
@@ -31,10 +31,11 @@
31
31
  * (`host_derive_entropy`) without ever holding the raw
32
32
  * root account secret. See the codec doc below.
33
33
  */
34
- import { p256 } from '@noble/curves/nist.js';
35
- import { Bytes, Enum, Struct, Tuple, Vector, _void, str } from 'scale-ts';
34
+ import { x25519 } from '@noble/curves/ed25519.js';
35
+ import { Bytes } from '@novasamatech/scale';
36
+ import { Enum, Struct, Tuple, Vector, _void, str } from 'scale-ts';
36
37
  const AccountIdCodec = Bytes(32);
37
- const PublicKeyCodec = Bytes(65);
38
+ const PublicKeyCodec = Bytes(32);
38
39
  const PrivateKeyCodec = Bytes(32);
39
40
  const EntropySourceCodec = Bytes(32);
40
41
  export const MetadataKey = Enum({
@@ -63,14 +64,14 @@ export const HandshakeSuccessV2 = Struct({
63
64
  identityAccountId: AccountIdCodec,
64
65
  rootAccountId: AccountIdCodec,
65
66
  identityChatPrivateKey: PrivateKeyCodec,
66
- /** PApp's P-256 SSO ECDH public key (`papp_encr_pub`). */
67
+ /** PApp's X25519 SSO ECDH public key (`papp_encr_pub`). */
67
68
  ssoEncPubKey: PublicKeyCodec,
68
69
  deviceEncPubKey: PublicKeyCodec,
69
70
  /** Layer-1 source for deterministic product entropy derivation (RFC-0007). */
70
71
  rootEntropySource: EntropySourceCodec,
71
72
  });
72
- /** Derive the identity chat P-256 public key (uncompressed) from its private scalar. */
73
- export const deriveIdentityChatPublicKey = (privateKey) => p256.getPublicKey(privateKey, false);
73
+ /** Derive the identity chat X25519 public key (32 bytes) from its private scalar. */
74
+ export const deriveIdentityChatPublicKey = (privateKey) => x25519.getPublicKey(privateKey);
74
75
  export const HandshakeStatusV2 = Enum({
75
76
  AllowanceAllocation: _void,
76
77
  });
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Decrypt the outer envelope of a `HandshakeResponseV2` statement payload.
3
3
  *
4
- * The answering side generates a one-shot P-256 keypair, performs ECDH against
5
- * the host device's encryption public key, and AES-GCM encrypts the sensitive
6
- * payload (the SCALE-encoded `EncryptedHandshakeResponseV2`) with a key
7
- * derived from the shared secret.
4
+ * The answering side generates a one-shot X25519 keypair, performs ECDH against
5
+ * the host device's encryption public key, and ChaCha20-Poly1305 encrypts the
6
+ * sensitive payload (the SCALE-encoded `EncryptedHandshakeResponseV2`) with a
7
+ * key derived from the shared secret.
8
8
  *
9
- * The shared-secret-to-AES-key derivation (HKDF-SHA256 over the ECDH X
10
- * coordinate) is delegated to `createEncryption(sharedSecret)` from
9
+ * The shared-secret-to-AEAD-key derivation (HKDF-SHA256 over the X25519 shared
10
+ * secret) is delegated to `createEncryption(sharedSecret)` from
11
11
  * `@novasamatech/statement-store` — byte-compatible with the existing V1
12
12
  * chat-request encryption helper, so we don't fork primitives here.
13
13
  */
@@ -1,21 +1,21 @@
1
1
  /**
2
2
  * Decrypt the outer envelope of a `HandshakeResponseV2` statement payload.
3
3
  *
4
- * The answering side generates a one-shot P-256 keypair, performs ECDH against
5
- * the host device's encryption public key, and AES-GCM encrypts the sensitive
6
- * payload (the SCALE-encoded `EncryptedHandshakeResponseV2`) with a key
7
- * derived from the shared secret.
4
+ * The answering side generates a one-shot X25519 keypair, performs ECDH against
5
+ * the host device's encryption public key, and ChaCha20-Poly1305 encrypts the
6
+ * sensitive payload (the SCALE-encoded `EncryptedHandshakeResponseV2`) with a
7
+ * key derived from the shared secret.
8
8
  *
9
- * The shared-secret-to-AES-key derivation (HKDF-SHA256 over the ECDH X
10
- * coordinate) is delegated to `createEncryption(sharedSecret)` from
9
+ * The shared-secret-to-AEAD-key derivation (HKDF-SHA256 over the X25519 shared
10
+ * secret) is delegated to `createEncryption(sharedSecret)` from
11
11
  * `@novasamatech/statement-store` — byte-compatible with the existing V1
12
12
  * chat-request encryption helper, so we don't fork primitives here.
13
13
  */
14
- import { p256 } from '@noble/curves/nist.js';
14
+ import { x25519 } from '@noble/curves/ed25519.js';
15
15
  import { createEncryption } from '@novasamatech/statement-store';
16
- const ecdhX = (privateKey, peerPublicKey) => p256.getSharedSecret(privateKey, peerPublicKey).slice(1, 33);
16
+ const ecdh = (privateKey, peerPublicKey) => x25519.getSharedSecret(privateKey, peerPublicKey);
17
17
  export const decryptResponseEnvelope = (deviceEncryptionPrivateKey, envelope) => {
18
- const shared = ecdhX(deviceEncryptionPrivateKey, envelope.tmpKey);
18
+ const shared = ecdh(deviceEncryptionPrivateKey, envelope.tmpKey);
19
19
  const result = createEncryption(shared).decrypt(envelope.encrypted);
20
20
  if (result.isErr())
21
21
  throw result.error;
@@ -29,8 +29,8 @@ export type HandshakePendingState = {
29
29
  export type HandshakeSuccessState = CodecType<typeof HandshakeSuccessV2> & {
30
30
  tag: 'Success';
31
31
  /**
32
- * Derived locally from `identityChatPrivateKey` via P-256 scalar
33
- * multiplication (uncompressed 65-byte form). Both sides MUST derive
32
+ * Derived locally from `identityChatPrivateKey` via X25519 scalar
33
+ * multiplication (32-byte form). Both sides MUST derive
34
34
  * identically; downstream session topics depend on it.
35
35
  */
36
36
  identityChatPublicKey: Uint8Array;
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Where:
8
8
  * - `statementAccountId` = the host's sr25519 device public key (32 bytes)
9
- * - `encryptionPublicKey` = the host's P-256 device public key (65 bytes uncompressed)
9
+ * - `encryptionPublicKey` = the host's X25519 device public key (32 bytes)
10
10
  *
11
11
  * Both sides compute the same topic/channel deterministically from the same
12
12
  * pubkeys carried in the QR-coded `VersionedHandshakeProposal::V2`, so they
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Where:
8
8
  * - `statementAccountId` = the host's sr25519 device public key (32 bytes)
9
- * - `encryptionPublicKey` = the host's P-256 device public key (65 bytes uncompressed)
9
+ * - `encryptionPublicKey` = the host's X25519 device public key (32 bytes)
10
10
  *
11
11
  * Both sides compute the same topic/channel deterministically from the same
12
12
  * pubkeys carried in the QR-coded `VersionedHandshakeProposal::V2`, so they
@@ -1,9 +1,10 @@
1
1
  import { gcm } from '@noble/ciphers/aes.js';
2
2
  import { blake2b } from '@noble/hashes/blake2.js';
3
+ import { Bytes } from '@novasamatech/scale';
3
4
  import { createSr25519Secret, deriveSr25519PublicKey } from '@novasamatech/statement-store';
4
5
  import { errAsync, fromPromise, okAsync } from 'neverthrow';
5
6
  import { fromHex, toHex } from 'polkadot-api/utils';
6
- import { Bytes, Option, Struct, str } from 'scale-ts';
7
+ import { Option, Struct, str } from 'scale-ts';
7
8
  import { getEncrPub, stringToBytes } from '../crypto.js';
8
9
  import { toError } from '../helpers/utils.js';
9
10
  const KEY = 'DeviceIdentity';
@@ -1,5 +1,6 @@
1
1
  import type { StatementStoreAdapter } from '@novasamatech/statement-store';
2
2
  import type { StorageAdapter } from '@novasamatech/storage-adapter';
3
+ import type { IdentityRepository } from '../../identity/types.js';
3
4
  import type { Callback } from '../../types.js';
4
5
  import type { AllowanceRepository } from '../allowance/index.js';
5
6
  import type { UserSecretRepository } from '../userSecretRepository.js';
@@ -12,8 +13,9 @@ type Params = {
12
13
  ssoSessionRepository: UserSessionRepository;
13
14
  userSecretRepository: UserSecretRepository;
14
15
  allowanceRepository: AllowanceRepository;
16
+ identityRepository: IdentityRepository;
15
17
  };
16
- export declare function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, allowanceRepository, statementStore, storage, }: Params): {
18
+ export declare function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, allowanceRepository, identityRepository, statementStore, storage, }: Params): {
17
19
  sessions: {
18
20
  read: () => UserSession[];
19
21
  subscribe: (callback: Callback<UserSession[]>) => () => void;