@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
@@ -1,7 +1,13 @@
1
1
  import type { CodecType } from 'scale-ts';
2
2
  export type SigningPayloadRequest = CodecType<typeof SigningPayloadRequestCodec>;
3
3
  export declare const SigningPayloadRequestCodec: import("scale-ts").Codec<{
4
- productAccountId: [string, number];
4
+ productAccountId: [string, {
5
+ tag: "Index";
6
+ value: number;
7
+ } | {
8
+ tag: "Raw";
9
+ value: Uint8Array<ArrayBufferLike>;
10
+ }];
5
11
  blockHash: `0x${string}`;
6
12
  blockNumber: `0x${string}`;
7
13
  era: `0x${string}`;
@@ -20,7 +26,13 @@ export declare const SigningPayloadRequestCodec: import("scale-ts").Codec<{
20
26
  }>;
21
27
  export type SigningRawRequest = CodecType<typeof SigningRawRequestCodec>;
22
28
  export declare const SigningRawRequestCodec: import("scale-ts").Codec<{
23
- productAccountId: [string, number];
29
+ productAccountId: [string, {
30
+ tag: "Index";
31
+ value: number;
32
+ } | {
33
+ tag: "Raw";
34
+ value: Uint8Array<ArrayBufferLike>;
35
+ }];
24
36
  data: {
25
37
  tag: "Bytes";
26
38
  value: Uint8Array<ArrayBufferLike>;
@@ -47,9 +59,33 @@ export declare const SignRawLegacyResponseCodec: import("scale-ts").Codec<{
47
59
  }>;
48
60
  export type SigningRequest = CodecType<typeof SigningRequestCodec>;
49
61
  export declare const SigningRequestCodec: import("scale-ts").Codec<{
62
+ tag: "Raw";
63
+ value: {
64
+ productAccountId: [string, {
65
+ tag: "Index";
66
+ value: number;
67
+ } | {
68
+ tag: "Raw";
69
+ value: Uint8Array<ArrayBufferLike>;
70
+ }];
71
+ data: {
72
+ tag: "Bytes";
73
+ value: Uint8Array<ArrayBufferLike>;
74
+ } | {
75
+ tag: "Payload";
76
+ value: string;
77
+ };
78
+ };
79
+ } | {
50
80
  tag: "Payload";
51
81
  value: {
52
- productAccountId: [string, number];
82
+ productAccountId: [string, {
83
+ tag: "Index";
84
+ value: number;
85
+ } | {
86
+ tag: "Raw";
87
+ value: Uint8Array<ArrayBufferLike>;
88
+ }];
53
89
  blockHash: `0x${string}`;
54
90
  blockNumber: `0x${string}`;
55
91
  era: `0x${string}`;
@@ -66,18 +102,6 @@ export declare const SigningRequestCodec: import("scale-ts").Codec<{
66
102
  mode: number | undefined;
67
103
  withSignedTransaction: boolean | void;
68
104
  };
69
- } | {
70
- tag: "Raw";
71
- value: {
72
- productAccountId: [string, number];
73
- data: {
74
- tag: "Bytes";
75
- value: Uint8Array<ArrayBufferLike>;
76
- } | {
77
- tag: "Payload";
78
- value: string;
79
- };
80
- };
81
105
  }>;
82
106
  export type SigningPayloadResponseData = CodecType<typeof SigningPayloadResponseDataCodec>;
83
107
  export declare const SigningPayloadResponseDataCodec: import("scale-ts").Codec<{
@@ -1,6 +1,6 @@
1
1
  import { AccountId, ProductAccountId } from '@novasamatech/host-api';
2
- import { Enum, Hex, OptionBool } from '@novasamatech/scale';
3
- import { Bytes, Option, Result, Struct, Vector, str, u32 } from 'scale-ts';
2
+ import { Bytes, Enum, Hex, OptionBool } from '@novasamatech/scale';
3
+ import { Option, Result, Struct, Vector, str, u32 } from 'scale-ts';
4
4
  export const SigningPayloadRequestCodec = Struct({
5
5
  productAccountId: ProductAccountId,
6
6
  blockHash: Hex(),
@@ -1,15 +1,22 @@
1
- import { ContextualAlias, ProductProofContext, RingLocation, RingVrfProof } from '@novasamatech/host-api';
1
+ import { ContextualAlias, ProductProofContext, RingLocation, RingVrfProof, VrfSignature } from '@novasamatech/host-api';
2
2
  import type { Encryption, StatementProver, StatementStoreAdapter } from '@novasamatech/statement-store';
3
3
  import type { StorageAdapter } from '@novasamatech/storage-adapter';
4
4
  import { ResultAsync } from 'neverthrow';
5
5
  import type { CodecType } from 'scale-ts';
6
+ import type { Identity, IdentityRepository } from '../../identity/types.js';
6
7
  import type { Callback } from '../../types.js';
8
+ import type { AllowanceRepository, AllowanceResourceKind } from '../allowance/index.js';
7
9
  import type { StoredUserSession } from '../userSessionRepository.js';
8
10
  import type { CreateTransactionLegacyRequest, CreateTransactionRequest } from './scale/createTransaction.js';
9
11
  import { RemoteMessageCodec } from './scale/remoteMessage.js';
10
12
  import type { ApAllocationOutcome, ResourceAllocationRequest } from './scale/resourceAllocation.js';
13
+ import type { SignVrfRequest } from './scale/signVrf.js';
11
14
  import type { SignRawLegacyRequest, SigningPayloadRequest, SigningPayloadResponseData, SigningRawRequest } from './scale/signing.js';
12
15
  export type UserSession = StoredUserSession & {
16
+ /** Read this session's persisted allowance slot-account key for a product/resource. */
17
+ readAllowance(productId: string, resource: AllowanceResourceKind): ResultAsync<Uint8Array | null, Error>;
18
+ /** Look up the on-chain identity of this session's user identity account. */
19
+ getIdentity(): ResultAsync<Identity | null, Error>;
13
20
  sendDisconnectMessage(): ResultAsync<void, Error>;
14
21
  abortPendingRequests(): ResultAsync<void, Error>;
15
22
  signPayload(payload: SigningPayloadRequest): ResultAsync<SigningPayloadResponseData, Error>;
@@ -19,14 +26,24 @@ export type UserSession = StoredUserSession & {
19
26
  createTransactionLegacy(payload: CreateTransactionLegacyRequest): ResultAsync<Uint8Array, Error>;
20
27
  getRingVrfAlias(callingProductId: string, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>): ResultAsync<CodecType<typeof ContextualAlias>, Error>;
21
28
  createRingVrfProof(callingProductId: string, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>, message: Uint8Array): ResultAsync<CodecType<typeof RingVrfProof>, Error>;
29
+ /**
30
+ * Fetches the sr25519 public key of `//product//{productId}` (RFC-0022).
31
+ *
32
+ * Consent-free and cacheable: one round trip per product, ever. Account
33
+ * public keys are then soft-derived locally from the returned subtree key.
34
+ */
35
+ getProductSubtree(productId: string): ResultAsync<Uint8Array, Error>;
36
+ signVrf(payload: SignVrfRequest): ResultAsync<CodecType<typeof VrfSignature>, Error>;
22
37
  requestResourceAllocation(request: ResourceAllocationRequest): ResultAsync<ApAllocationOutcome[], Error>;
23
38
  subscribe(callback: Callback<CodecType<typeof RemoteMessageCodec>, ResultAsync<boolean, Error>>): VoidFunction;
24
39
  dispose(): void;
25
40
  };
26
- export declare function createUserSession({ userSession, statementStore, encryption, storage, prover, }: {
41
+ export declare function createUserSession({ userSession, statementStore, encryption, storage, prover, allowanceRepository, identityRepository, }: {
27
42
  userSession: StoredUserSession;
28
43
  statementStore: StatementStoreAdapter;
29
44
  encryption: Encryption;
30
45
  storage: StorageAdapter;
31
46
  prover: StatementProver;
47
+ allowanceRepository: AllowanceRepository;
48
+ identityRepository: IdentityRepository;
32
49
  }): UserSession;
@@ -1,9 +1,10 @@
1
- import { ContextualAlias, ProductProofContext, RingLocation, RingVrfProof } from '@novasamatech/host-api';
1
+ import { ContextualAlias, ProductProofContext, RingLocation, RingVrfProof, VrfSignature } from '@novasamatech/host-api';
2
2
  import { enumValue } from '@novasamatech/scale';
3
3
  import { createSession } from '@novasamatech/statement-store';
4
4
  import { fieldListView } from '@novasamatech/storage-adapter';
5
5
  import { nanoid } from 'nanoid';
6
6
  import { ResultAsync, err, ok, okAsync } from 'neverthrow';
7
+ import { toHex } from 'polkadot-api/utils';
7
8
  import { emitHostPappDebugMessage } from '../../debugBus.js';
8
9
  import { createAsyncTaskPool } from '../../helpers/createAsyncTaskPool.js';
9
10
  import { toError } from '../../helpers/utils.js';
@@ -80,7 +81,7 @@ function withHostActionTrace(result, messageId, sessionId) {
80
81
  });
81
82
  });
82
83
  }
83
- export function createUserSession({ userSession, statementStore, encryption, storage, prover, }) {
84
+ export function createUserSession({ userSession, statementStore, encryption, storage, prover, allowanceRepository, identityRepository, }) {
84
85
  const requestQueue = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
85
86
  // Shared abort handle for everything currently on the request queue.
86
87
  // abortPendingRequests() fires it to drop the in-flight task plus anything
@@ -111,6 +112,12 @@ export function createUserSession({ userSession, statementStore, encryption, sto
111
112
  });
112
113
  return {
113
114
  ...userSession,
115
+ readAllowance(productId, resource) {
116
+ return allowanceRepository.read(userSession.id, productId, resource);
117
+ },
118
+ getIdentity() {
119
+ return identityRepository.getIdentity(toHex(userSession.identityAccountId));
120
+ },
114
121
  signPayload(payload) {
115
122
  return enqueue(() => {
116
123
  const messageId = nanoid();
@@ -280,6 +287,42 @@ export function createUserSession({ userSession, statementStore, encryption, sto
280
287
  return withHostActionTrace(awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value)), messageId, userSession.id);
281
288
  });
282
289
  },
290
+ getProductSubtree(productId) {
291
+ return enqueue(() => {
292
+ const messageId = nanoid();
293
+ const data = enumValue('v1', enumValue('ProductSubtreeRequest', { productId }));
294
+ emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
295
+ const responseFilter = (message) => {
296
+ if (message.data.tag === 'v1' &&
297
+ message.data.value.tag === 'ProductSubtreeResponse' &&
298
+ message.data.value.value.respondingTo === messageId) {
299
+ return message.data.value.value.payload;
300
+ }
301
+ };
302
+ const request = session.request(RemoteMessageCodec, { messageId, data });
303
+ const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
304
+ const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value.productPublicKey) : err(new Error(result.value)));
305
+ return withHostActionTrace(withQueueTimeout(inner, 'getProductSubtree'), messageId, userSession.id);
306
+ });
307
+ },
308
+ signVrf(payload) {
309
+ return enqueue(() => {
310
+ const messageId = nanoid();
311
+ const data = enumValue('v1', enumValue('SignVrfRequest', payload));
312
+ emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
313
+ const responseFilter = (message) => {
314
+ if (message.data.tag === 'v1' &&
315
+ message.data.value.tag === 'SignVrfResponse' &&
316
+ message.data.value.value.respondingTo === messageId) {
317
+ return message.data.value.value.payload;
318
+ }
319
+ };
320
+ const request = session.request(RemoteMessageCodec, { messageId, data });
321
+ const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
322
+ const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
323
+ return withHostActionTrace(withQueueTimeout(inner, 'signVrf'), messageId, userSession.id);
324
+ });
325
+ },
283
326
  requestResourceAllocation(payload) {
284
327
  return enqueue(() => {
285
328
  const messageId = nanoid();
@@ -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, Struct } from 'scale-ts';
6
+ import { Struct } from 'scale-ts';
6
7
  import { BrandedBytesCodec, stringToBytes } from '../crypto.js';
7
8
  import { toError } from '../helpers/utils.js';
8
9
  const StoredUserSecretsCodec = Struct({
@@ -1,30 +1,31 @@
1
+ import { Bytes } from '@novasamatech/scale';
1
2
  import { AccountIdCodec, LocalSessionAccountCodec, RemoteSessionAccountCodec } from '@novasamatech/statement-store';
2
3
  import { fieldListView } from '@novasamatech/storage-adapter';
3
4
  import { nanoid } from 'nanoid';
4
5
  import { fromHex, toHex } from 'polkadot-api/utils';
5
- import { Bytes, Struct, Vector, str } from 'scale-ts';
6
+ import { Struct, Vector, str } from 'scale-ts';
6
7
  const storedUserSessionCodec = Struct({
7
8
  id: str,
8
9
  localAccount: LocalSessionAccountCodec,
9
10
  remoteAccount: RemoteSessionAccountCodec,
10
11
  rootAccountId: AccountIdCodec,
11
12
  identityAccountId: AccountIdCodec,
12
- identityChatPublicKey: Bytes(65),
13
- // `papp_encr_pub` (65-byte uncompressed P-256). Persisted so the host can
13
+ identityChatPublicKey: Bytes(32),
14
+ // `papp_encr_pub` (32-byte X25519). Persisted so the host can
14
15
  // rebuild its SSO session transport (`shared_secret_session =
15
16
  // ECDH(host_encr_secret, ssoEncPubKey)`) on a cold start without re-running
16
17
  // the handshake.
17
- ssoEncPubKey: Bytes(65),
18
+ ssoEncPubKey: Bytes(32),
18
19
  // RFC-0007 layer-1 `rootEntropySource` from the handshake; consumed by the
19
20
  // host's `host_derive_entropy` handler via `deriveProductEntropyFromSource`.
20
21
  rootEntropySource: Bytes(32),
21
- // Encryption public key of the authorising PApp device (65-byte uncompressed
22
- // P-256), lifted from `HandshakeResponseV2.deviceEncPubKey`. Distinct from
22
+ // Encryption public key of the authorising PApp device (32-byte X25519),
23
+ // lifted from `HandshakeResponseV2.deviceEncPubKey`. Distinct from
23
24
  // `ssoEncPubKey` (the SSO session keypair) and from `remoteAccount.publicKey`
24
25
  // (the derived SSO shared secret): this is the peer device's long-lived ECDH
25
26
  // key, used by the host's device-sync channel to address the paired device.
26
27
  // Always present — `HandshakeResponseV2` carries it for every V2 pairing.
27
- deviceEncPubKey: Bytes(65),
28
+ deviceEncPubKey: Bytes(32),
28
29
  });
29
30
  export function createStoredUserSession(localAccount, remoteAccount, rootAccountId, extras) {
30
31
  return {
@@ -43,7 +44,11 @@ export const createUserSessionRepository = (storage) => {
43
44
  const codec = Vector(storedUserSessionCodec);
44
45
  return fieldListView({
45
46
  storage,
46
- key: 'SsoSessionsV3',
47
+ // V4: the encryption keys shrank from 65-byte P-256 to 32-byte X25519
48
+ // (CHAT-RFC-0004). Fixed-size `Bytes` does not length-check on decode, so a
49
+ // V3 blob would decode misaligned into a session that looks valid but
50
+ // carries garbage keys — bump the key so old blobs are dropped instead.
51
+ key: 'SsoSessionsV4',
47
52
  from: x => codec.dec(fromHex(x)),
48
53
  to: x => toHex(codec.enc(x)),
49
54
  });
@@ -5,14 +5,14 @@ import { describe, expect, it } from 'vitest';
5
5
  import { createStoredUserSession, createUserSessionRepository } from './userSessionRepository.js';
6
6
  const bytes = (len, fill) => new Uint8Array(len).fill(fill);
7
7
  // remoteAccount.publicKey is a fixed 32-byte field (the SSO shared secret),
8
- // which is exactly why the peer device's 65-byte encryption key needs its own
8
+ // which is exactly why the peer device's 32-byte encryption key needs its own
9
9
  // `deviceEncPubKey` field rather than being squeezed in here.
10
10
  const SHARED_SECRET = bytes(32, 0x07);
11
- const DEVICE_ENC_PUB = bytes(65, 0x04);
11
+ const DEVICE_ENC_PUB = bytes(32, 0x04);
12
12
  const baseExtras = {
13
13
  identityAccountId: createAccountId(bytes(32, 0x11)),
14
- identityChatPublicKey: bytes(65, 0x22),
15
- ssoEncPubKey: bytes(65, 0x33),
14
+ identityChatPublicKey: bytes(32, 0x22),
15
+ ssoEncPubKey: bytes(32, 0x33),
16
16
  rootEntropySource: bytes(32, 0x44),
17
17
  };
18
18
  const makeSession = (deviceEncPubKey = DEVICE_ENC_PUB) => createStoredUserSession(createLocalSessionAccount(createAccountId(bytes(32, 0x01))), createRemoteSessionAccount(createAccountId(bytes(32, 0x02)), SHARED_SECRET), createAccountId(bytes(32, 0x55)), { ...baseExtras, deviceEncPubKey });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/host-papp",
3
3
  "type": "module",
4
- "version": "0.8.11",
4
+ "version": "0.9.0",
5
5
  "description": "Polkadot app integration",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -34,14 +34,14 @@
34
34
  "@noble/ciphers": "2.2.0",
35
35
  "@noble/curves": "2.2.0",
36
36
  "@noble/hashes": "2.2.0",
37
- "@novasamatech/host-api": "0.8.11",
38
- "@novasamatech/scale": "0.8.11",
39
- "@novasamatech/statement-store": "0.8.11",
40
- "@novasamatech/storage-adapter": "0.8.11",
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
41
  "@polkadot-api/utils": "^0.4.0",
42
- "@polkadot-labs/hdkd-helpers": "^0.0.30",
43
- "nanoevents": "9.1.0",
44
- "nanoid": "5.1.11",
42
+ "@polkadot-labs/hdkd-helpers": "^0.0.31",
43
+ "nanoevents": "10.0.0",
44
+ "nanoid": "6.0.0",
45
45
  "neverthrow": "^8.2.0",
46
46
  "polkadot-api": ">=2",
47
47
  "rxjs": "^7.8.2",